Compare commits

...

41 Commits

Author SHA1 Message Date
MHSanaei
f3d47ebb3f Refactor TLS peer cert verification settings
Removed verifyPeerCertByNames and pinnedPeerCertSha256 from inbound TLS settings and UI. Added verifyPeerCertByName and pinnedPeerCertSha256 to outbound TLS settings and updated the outbound form to support these fields. This change streamlines and clarifies certificate verification configuration between inbound and outbound settings.
2026-02-01 14:03:46 +01:00
MHSanaei
06c49b92f8 v2.8.9 2026-02-01 04:05:02 +01:00
MHSanaei
e35213bc73 Update Xray-core to v26.1.31 and related dependencies
Bump Xray-core version to v26.1.31 in build scripts and server logic. Update Go dependencies including gopsutil, bytedance/sonic, circl, miekg/dns, go-proxyproto, sagernet/sing, and others to their latest versions. Adjust version check in GetXrayVersions to require at least v26.1.31.
2026-02-01 03:30:09 +01:00
MHSanaei
aa6a886977 Add UDP hop interval min/max support for Hysteria
Replaces single UDP hop interval with separate min and max values in Hysteria stream settings. Updates model, JSON serialization, URL param parsing, and form fields for backward compatibility and enhanced configuration flexibility.
2026-02-01 03:20:29 +01:00
MHSanaei
9d603c5ad2 Add pinnedPeerCertSha256 support to TLS settings
Introduces the pinnedPeerCertSha256 field to TlsStreamSettings in the JS model and adds a corresponding input in the TLS settings form. This allows users to specify SHA256 fingerprints for peer certificate pinning, enhancing security configuration options.
2026-02-01 03:12:54 +01:00
MHSanaei
a973fa6d68 XHTTP transport: New options for bypassing CDN's detection
https://github.com/XTLS/Xray-core/pull/5414
2026-02-01 02:58:18 +01:00
MHSanaei
3af6497577 inbound : finalmask 2026-02-01 02:36:57 +01:00
MHSanaei
c59f54bb0e outbound: finalmask 2026-02-01 01:56:23 +01:00
lillinlin
6b3da4fe5e Update reality_targets.js (#3724) 2026-01-31 23:50:29 +01:00
Farhad H. P. Shirvan
ea0da32e81 fix: rename verifyPeerCertInNames to verifyPeerCertByName to be compatible with xray-core v26.1.31 (#3723) 2026-01-31 19:50:08 +01:00
Sam Mosleh
d5ea8d0f38 Fix default CA by enforcing it everywhere (#3719) 2026-01-30 16:35:24 +01:00
Danil S.
fd5f591737 feat: more subscription information fields (#3701)
* feat: more subscription information fields

* fix: incorrect translation

* feat: implement field for Happ custom routing rules
2026-01-26 23:06:01 +01:00
Sam Mosleh
8a4c9a98cb Fix modifying default CA (#3708) 2026-01-26 23:05:15 +01:00
sviatoslav-gusev
70b365171f feat: add option to use existing custom SSL certificates (#3688) 2026-01-21 16:47:36 +01:00
mr-shura
328ba3b45e fix Telegram bot ignores reverse proxy setting #3673 (#3684)
Refactor URL construction to use pre-configured URIs if available, otherwise fallback to default scheme and host.
2026-01-19 12:33:17 +01:00
Nebulosa
5370b6943a Add hysteria2 protocol in hint text (#3686) 2026-01-19 12:31:49 +01:00
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
72 changed files with 2818 additions and 1035 deletions

View File

@@ -18,6 +18,7 @@ on:
- 'go.mod'
- 'go.sum'
- 'x-ui.service.debian'
- 'x-ui.service.arch'
- 'x-ui.service.rhel'
jobs:
@@ -80,6 +81,7 @@ jobs:
mkdir x-ui
cp xui-release 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.sh x-ui/
mv x-ui/xui-release x-ui/x-ui
@@ -87,7 +89,7 @@ jobs:
cd x-ui/bin
# 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.31/"
if [ "${{ matrix.platform }}" == "amd64" ]; then
wget -q ${Xray_URL}Xray-linux-64.zip
unzip Xray-linux-64.zip
@@ -185,7 +187,7 @@ jobs:
cd x-ui\bin
# 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.31/"
Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
Remove-Item "Xray-windows-64.zip"
@@ -223,4 +225,4 @@ jobs:
file: x-ui-windows-amd64.zip
asset_name: x-ui-windows-amd64.zip
overwrite: true
prerelease: true
prerelease: true

View File

@@ -27,7 +27,7 @@ case $1 in
esac
mkdir -p 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.31/Xray-linux-${ARCH}.zip"
unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
mv xray "xray-linux-${FNAME}"

View File

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

View File

@@ -1 +1 @@
2.8.6
2.8.9

View File

@@ -80,9 +80,12 @@ type HistoryOfSeeders struct {
// GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
listen := i.Listen
if listen != "" {
listen = fmt.Sprintf("\"%v\"", listen)
// Default to 0.0.0.0 (all interfaces) when listen is empty
// 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{
Listen: json_util.RawMessage(listen),
Port: i.Port,

52
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/mhsanaei/3x-ui/v2
go 1.25.5
go 1.25.6
require (
github.com/gin-contrib/gzip v1.2.5
@@ -11,20 +11,20 @@ require (
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
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/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.2.4
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.25.12
github.com/shirou/gopsutil/v4 v4.26.1
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/xtls/xray-core v1.251208.0
github.com/xtls/xray-core v1.260131.0
go.uber.org/atomic v1.11.0
golang.org/x/crypto v0.46.0
golang.org/x/sys v0.39.0
golang.org/x/text v0.32.0
golang.org/x/crypto v0.47.0
golang.org/x/sys v0.40.0
golang.org/x/text v0.33.0
google.golang.org/grpc v1.78.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
@@ -33,12 +33,12 @@ require (
require (
github.com/Azure/go-ntlmssp v0.1.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/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cloudflare/circl v1.6.2 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
@@ -47,7 +47,7 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.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/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
@@ -57,30 +57,27 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // 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/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.33 // indirect
github.com/miekg/dns v1.1.69 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // 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.9.2 // 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/quic-go v0.58.0 // indirect
github.com/refraction-networking/utls v1.8.1 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/refraction-networking/utls v1.8.2 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagernet/sing v0.7.14 // indirect
github.com/sagernet/sing v0.7.18 // indirect
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fastjson v1.6.7 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect
@@ -89,15 +86,16 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // 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/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/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-20260128011058-8636f8732409 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
)

103
go.sum
View File

@@ -6,22 +6,21 @@ 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/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
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/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ=
github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM=
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
@@ -57,8 +56,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/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-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
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/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -106,8 +105,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/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/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
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/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -122,15 +121,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-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
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.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/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-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
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/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v1.4.0 h1:z74W5lfOTgLplQXuZPjDsRvvvI0iQatO2gp/XZz7s3I=
github.com/mymmrac/telego v1.4.0/go.mod h1:u9fKXZSOCOdMj6K0U69fQqeAvDE+2RGkHKkDksijp3o=
github.com/mymmrac/telego v1.5.0 h1:VjBDZcSpEQim1Y3JX2WCsF/PJqOA2DKfZknXUvtKCnw=
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/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
@@ -139,32 +138,28 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pires/go-proxyproto v0.9.2 h1:H1UdHn695zUVVmB0lQ354lOWHOy6TZSpzBl3tgN0s1U=
github.com/pires/go-proxyproto v0.9.2/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
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/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo=
github.com/refraction-networking/utls v1.8.1/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/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sagernet/sing v0.7.14 h1:5QQRDCUvYNOMyVp3LuK/hYEBAIv0VsbD3x/l9zH467s=
github.com/sagernet/sing v0.7.14/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.7.18 h1:iZHkaru1/MoHugx3G+9S3WG4owMewKO/KvieE2Pzk4E=
github.com/sagernet/sing v0.7.18/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo=
github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -172,7 +167,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
@@ -187,12 +181,10 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
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/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
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/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
@@ -203,8 +195,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/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/xray-core v1.251208.0 h1:9jIXi+9KXnfmT5esSYNf9VAQlQkaAP8bG413B0eyAes=
github.com/xtls/xray-core v1.251208.0/go.mod h1:kclzboEF0g6VBrp9/NXm8C0Aj64SDBt52OfthH1LSr4=
github.com/xtls/xray-core v1.260131.0 h1:gPBykLhUvRZ8sfubNerkwWqV3c15UtmSYQG2cgKqrV4=
github.com/xtls/xray-core v1.260131.0/go.mod h1:cxzYFZrxu1B1NtPjHsqv4UzgDvRA71mV4rXYH4KtO7Q=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
@@ -231,12 +223,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=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
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.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
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/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -245,22 +239,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.6.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.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
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/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
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/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/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
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-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
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/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
@@ -271,14 +265,13 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
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/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h1:Lk6hARj5UPY47dBep70OD/TIMwikJ5fGUGX0Rm3Xigk=
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
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_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() {
case "${release}" in
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)
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)
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
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
;;
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)
zypper refresh && zypper -q install -y curl tar timezone openssl socat
zypper refresh && zypper -q install -y curl tar timezone socat ca-certificates
;;
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
}
@@ -130,7 +147,7 @@ setup_ssl_certificate() {
echo -e "${green}Issuing SSL certificate for ${domain}...${plain}"
echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force
if [ $? -ne 0 ]; then
@@ -154,7 +171,9 @@ setup_ssl_certificate() {
# Enable auto-renew
~/.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
local webCertFile="/root/cert/${domain}/fullchain.pem"
@@ -170,56 +189,155 @@ setup_ssl_certificate() {
fi
}
# Fallback: generate a self-signed certificate (not publicly trusted)
setup_self_signed_certificate() {
local name="$1" # domain or IP to place in SAN
local certDir="/root/cert/selfsigned"
# Issue Let's Encrypt IP certificate with shortlived profile (~6 days validity)
# Requires acme.sh and port 80 open for HTTP-01 challenge
setup_ip_certificate() {
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"
local sanExt=""
if is_ip "$name"; then
sanExt="IP:${name}"
else
sanExt="DNS:${name}"
# Check for acme.sh
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
install_acme
if [ $? -ne 0 ]; then
echo -e "${red}Failed to install acme.sh${plain}"
return 1
fi
fi
# Use -addext if supported; fallback to config file if needed
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
# 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}"
# Validate IP address
if [[ -z "$ipv4" ]]; then
echo -e "${red}IPv4 address is required${plain}"
return 1
fi
chmod 755 ${certDir}/* 2>/dev/null
${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" >/dev/null 2>&1
echo -e "${yellow}Self-signed certificate configured. Browsers will show a warning.${plain}"
if ! is_ipv4 "$ipv4"; then
echo -e "${red}Invalid IPv4 address: $ipv4${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 --force >/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
}
@@ -296,7 +414,7 @@ ssl_cert_issue() {
systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null
# issue the certificate
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force
if [ $? -ne 0 ]; then
echo -e "${red}Issuing certificate failed, please check logs.${plain}"
@@ -352,14 +470,18 @@ ssl_cert_issue() {
if [ $? -ne 0 ]; then
echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}"
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
echo -e "${green}Auto renew succeeded, certificate details:${plain}"
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
# Restart panel
# start panel
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
@@ -387,7 +509,7 @@ ssl_cert_issue() {
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
prompt_and_setup_ssl() {
local panel_port="$1"
@@ -397,20 +519,22 @@ prompt_and_setup_ssl() {
local ssl_choice=""
echo -e "${yellow}Choose SSL certificate setup method:${plain}"
echo -e "${green}1.${plain} Let's Encrypt (domain required, recommended)"
echo -e "${green}2.${plain} Self-signed certificate (not publicly trusted)"
read -rp "Choose an option (default 2): " ssl_choice
echo -e "${green}1.${plain} Let's Encrypt for Domain (90-day validity, auto-renews)"
echo -e "${green}2.${plain} Let's Encrypt for IP Address (6-day validity, auto-renews)"
echo -e "${green}3.${plain} Custom SSL Certificate (Path to existing files)"
echo -e "${blue}Note:${plain} Options 1 & 2 require port 80 open. Option 3 requires manual paths."
read -rp "Choose an option (default 2 for IP): " ssl_choice
ssl_choice="${ssl_choice// /}" # Trim whitespace
# Default to 2 (self-signed) if not 1
if [[ "$ssl_choice" != "1" ]]; then
# Default to 2 (IP cert) if input is empty or invalid (not 1 or 3)
if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" ]]; then
ssl_choice="2"
fi
case "$ssl_choice" in
1)
# 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
# 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}')
@@ -423,28 +547,89 @@ prompt_and_setup_ssl() {
fi
;;
2)
# User chose self-signed option
# Stop panel if running
# User chose Let's Encrypt IP certificate option
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
rc-service x-ui stop >/dev/null 2>&1
else
systemctl stop x-ui >/dev/null 2>&1
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
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
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}"
fi
# Start panel after SSL is configured
if [[ $release == "alpine" ]]; then
rc-service x-ui start >/dev/null 2>&1
;;
3)
# User chose Custom Paths (User Provided) option
echo -e "${green}Using custom existing certificate...${plain}"
local custom_cert=""
local custom_key=""
local custom_domain=""
# 3.1 Request Domain to compose Panel URL later
read -rp "Please enter domain name certificate issued for: " custom_domain
custom_domain="${custom_domain// /}" # Убираем пробелы
# 3.2 Loop for Certificate Path
while true; do
read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert
# Strip quotes if present
custom_cert=$(echo "$custom_cert" | tr -d '"' | tr -d "'")
if [[ -f "$custom_cert" && -r "$custom_cert" && -s "$custom_cert" ]]; then
break
elif [[ ! -f "$custom_cert" ]]; then
echo -e "${red}Error: File does not exist! Try again.${plain}"
elif [[ ! -r "$custom_cert" ]]; then
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
else
echo -e "${red}Error: File is empty!${plain}"
fi
done
# 3.3 Loop for Private Key Path
while true; do
read -rp "Input private key path (keywords: .key / privatekey): " custom_key
# Strip quotes if present
custom_key=$(echo "$custom_key" | tr -d '"' | tr -d "'")
if [[ -f "$custom_key" && -r "$custom_key" && -s "$custom_key" ]]; then
break
elif [[ ! -f "$custom_key" ]]; then
echo -e "${red}Error: File does not exist! Try again.${plain}"
elif [[ ! -r "$custom_key" ]]; then
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
else
echo -e "${red}Error: File is empty!${plain}"
fi
done
# 3.4 Apply Settings via x-ui binary
${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" >/dev/null 2>&1
# Set SSL_HOST for composing Panel URL
if [[ -n "$custom_domain" ]]; then
SSL_HOST="$custom_domain"
else
systemctl start x-ui >/dev/null 2>&1
SSL_HOST="${server_ip}"
fi
echo -e "${green}✓ Custom certificate paths applied.${plain}"
echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}"
systemctl restart x-ui >/dev/null 2>&1 || rc-service x-ui restart >/dev/null 2>&1
;;
*)
echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
@@ -497,7 +682,7 @@ config_after_install() {
echo -e "${green} SSL Certificate Setup (MANDATORY) ${plain}"
echo -e "${green}═══════════════════════════════════════════${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 ""
prompt_and_setup_ssl "${config_port}" "${config_webBasePath}" "${server_ip}"
@@ -527,7 +712,7 @@ config_after_install() {
echo -e "${green}═══════════════════════════════════════════${plain}"
echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${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 ""
prompt_and_setup_ssl "${existing_port}" "${config_webBasePath}" "${server_ip}"
echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${config_webBasePath}${plain}"
@@ -552,7 +737,7 @@ config_after_install() {
echo -e "${green}Username, Password, and WebBasePath are properly set.${plain}"
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
existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
if [[ -z "$existing_cert" ]]; then
@@ -560,7 +745,7 @@ config_after_install() {
echo -e "${green}═══════════════════════════════════════════${plain}"
echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${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 ""
prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}"
echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${existing_webBasePath}${plain}"
@@ -587,7 +772,7 @@ install_x-ui() {
fi
fi
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
echo -e "${red}Downloading x-ui failed, please be sure that your server can access GitHub ${plain}"
exit 1
@@ -604,7 +789,7 @@ install_x-ui() {
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"
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
echo -e "${red}Download x-ui $1 failed, please check if the version exists ${plain}"
exit 1
@@ -693,6 +878,15 @@ install_x-ui() {
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
echo -e "${green}Found x-ui.service.rhel in extracted files, installing...${plain}"
@@ -712,6 +906,9 @@ install_x-ui() {
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
;;
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
;;

View File

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

View File

@@ -153,6 +153,31 @@ func (s *Server) initRouter() (*gin.Engine, error) {
SubTitle = ""
}
SubSupportUrl, err := s.settingService.GetSubSupportUrl()
if err != nil {
SubSupportUrl = ""
}
SubProfileUrl, err := s.settingService.GetSubProfileUrl()
if err != nil {
SubProfileUrl = ""
}
SubAnnounce, err := s.settingService.GetSubAnnounce()
if err != nil {
SubAnnounce = ""
}
SubEnableRouting, err := s.settingService.GetSubEnableRouting()
if err != nil {
return nil, err
}
SubRoutingRules, err := s.settingService.GetSubRoutingRules()
if err != nil {
SubRoutingRules = ""
}
// set per-request localizer from headers/cookies
engine.Use(locale.LocalizerMiddleware())
@@ -231,7 +256,8 @@ func (s *Server) initRouter() (*gin.Engine, error) {
s.sub = NewSUBController(
g, LinksPath, JsonPath, subJsonEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates,
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle)
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle, SubSupportUrl,
SubProfileUrl, SubAnnounce, SubEnableRouting, SubRoutingRules)
return engine, nil
}

View File

@@ -4,6 +4,7 @@ import (
"encoding/base64"
"fmt"
"strings"
"strconv"
"github.com/mhsanaei/3x-ui/v2/config"
@@ -12,12 +13,17 @@ import (
// SUBController handles HTTP requests for subscription links and JSON configurations.
type SUBController struct {
subTitle string
subPath string
subJsonPath string
jsonEnabled bool
subEncrypt bool
updateInterval string
subTitle string
subSupportUrl string
subProfileUrl string
subAnnounce string
subEnableRouting bool
subRoutingRules string
subPath string
subJsonPath string
jsonEnabled bool
subEncrypt bool
updateInterval string
subService *SubService
subJsonService *SubJsonService
@@ -38,18 +44,28 @@ func NewSUBController(
jsonMux string,
jsonRules string,
subTitle string,
subSupportUrl string,
subProfileUrl string,
subAnnounce string,
subEnableRouting bool,
subRoutingRules string,
) *SUBController {
sub := NewSubService(showInfo, rModel)
a := &SUBController{
subTitle: subTitle,
subPath: subPath,
subJsonPath: jsonPath,
jsonEnabled: jsonEnabled,
subEncrypt: encrypt,
updateInterval: update,
subTitle: subTitle,
subSupportUrl: subSupportUrl,
subProfileUrl: subProfileUrl,
subAnnounce: subAnnounce,
subEnableRouting: subEnableRouting,
subRoutingRules: subRoutingRules,
subPath: subPath,
subJsonPath: jsonPath,
jsonEnabled: jsonEnabled,
subEncrypt: encrypt,
updateInterval: update,
subService: sub,
subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),
subService: sub,
subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),
}
a.initRouter(g)
return a
@@ -127,7 +143,7 @@ func (a *SUBController) subs(c *gin.Context) {
// Add headers
header := fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle)
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle, a.subSupportUrl, a.subProfileUrl, a.subAnnounce, a.subEnableRouting, a.subRoutingRules)
if a.subEncrypt {
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
@@ -145,17 +161,31 @@ func (a *SUBController) subJsons(c *gin.Context) {
if err != nil || len(jsonSub) == 0 {
c.String(400, "Error!")
} else {
// Add headers
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle)
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle, a.subSupportUrl, a.subProfileUrl, a.subAnnounce, a.subEnableRouting, a.subRoutingRules)
c.String(200, jsonSub)
}
}
// ApplyCommonHeaders sets common HTTP headers for subscription responses including user info, update interval, and profile title.
func (a *SUBController) ApplyCommonHeaders(c *gin.Context, header, updateInterval, profileTitle string) {
func (a *SUBController) ApplyCommonHeaders(
c *gin.Context,
header,
updateInterval,
profileTitle string,
profileSupportUrl string,
profileUrl string,
profileAnnounce string,
profileEnableRouting bool,
profileRoutingRules string,
) {
c.Writer.Header().Set("Subscription-Userinfo", header)
c.Writer.Header().Set("Profile-Update-Interval", updateInterval)
c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileTitle)))
c.Writer.Header().Set("Support-Url", profileSupportUrl)
c.Writer.Header().Set("Profile-Web-Page-Url", profileUrl)
c.Writer.Header().Set("Announce", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileAnnounce)))
c.Writer.Header().Set("Routing-Enable", strconv.FormatBool(profileEnableRouting))
c.Writer.Header().Set("Routing", profileRoutingRules)
}

View File

@@ -4,6 +4,7 @@ import (
_ "embed"
"encoding/json"
"fmt"
"maps"
"strings"
"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...)
newConfigJson := make(map[string]any)
for key, value := range s.configJson {
newConfigJson[key] = value
}
maps.Copy(newConfigJson, s.configJson)
newConfigJson["outbounds"] = newOutbounds
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)
if len(externalProxies) > 0 {
links := ""
for index, externalProxy := range externalProxies {
links := make([]string, 0, len(externalProxies))
for _, externalProxy := range externalProxies {
ep, _ := externalProxy.(map[string]any)
newSecurity, _ := ep["forceTls"].(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))
if index > 0 {
links += "\n"
}
links += url.String()
links = append(links, url.String())
}
return links
return strings.Join(links, "\n")
}
link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port)

357
update.sh
View File

@@ -78,7 +78,24 @@ is_ip() {
is_ipv4 "$1" || is_ipv6 "$1"
}
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() {
@@ -91,29 +108,29 @@ install_base() {
echo -e "${green}Updating and install dependency packages...${plain}"
case "${release}" in
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)
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)
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
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
;;
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)
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)
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
}
@@ -156,7 +173,7 @@ setup_ssl_certificate() {
echo -e "${green}Issuing SSL certificate for ${domain}...${plain}"
echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force
if [ $? -ne 0 ]; then
@@ -180,7 +197,8 @@ setup_ssl_certificate() {
# Enable auto-renew
~/.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
local webCertFile="/root/cert/${domain}/fullchain.pem"
@@ -196,57 +214,156 @@ setup_ssl_certificate() {
fi
}
# Fallback: generate a self-signed certificate (not publicly trusted)
setup_self_signed_certificate() {
local name="$1" # domain or IP to place in SAN
local certDir="/root/cert/selfsigned"
# Issue Let's Encrypt IP certificate with shortlived profile (~6 days validity)
# Requires acme.sh and port 80 open for HTTP-01 challenge
setup_ip_certificate() {
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"
local sanExt=""
if is_ip "$name"; then
sanExt="IP:${name}"
else
sanExt="DNS:${name}"
# Check for acme.sh
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
install_acme
if [ $? -ne 0 ]; then
echo -e "${red}Failed to install acme.sh${plain}"
return 1
fi
fi
# Try -addext; fallback to config if not supported
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
echo -e "${red}Failed to generate self-signed certificate${plain}"
# Validate IP address
if [[ -z "$ipv4" ]]; then
echo -e "${red}IPv4 address is required${plain}"
return 1
fi
chmod 755 ${certDir}/* 2>/dev/null
${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" >/dev/null 2>&1
echo -e "${yellow}Self-signed certificate configured. Browsers will show a warning.${plain}"
if ! is_ipv4 "$ipv4"; then
echo -e "${red}Invalid IPv4 address: $ipv4${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 --force >/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
}
# Comprehensive manual SSL certificate issuance via acme.sh
ssl_cert_issue() {
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##')
@@ -320,7 +437,7 @@ ssl_cert_issue() {
systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null
# issue the certificate
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force
if [ $? -ne 0 ]; then
echo -e "${red}Issuing certificate failed, please check logs.${plain}"
@@ -376,11 +493,13 @@ ssl_cert_issue() {
if [ $? -ne 0 ]; then
echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}"
ls -lah /root/cert/${domain}/
chmod 755 $certPath/*
chmod 600 $certPath/privkey.pem
chmod 644 $certPath/fullchain.pem
else
echo -e "${green}Auto renew succeeded, certificate details:${plain}"
ls -lah /root/cert/${domain}/
chmod 755 $certPath/*
chmod 600 $certPath/privkey.pem
chmod 644 $certPath/fullchain.pem
fi
# Restart panel
@@ -410,7 +529,7 @@ ssl_cert_issue() {
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
prompt_and_setup_ssl() {
local panel_port="$1"
@@ -420,20 +539,22 @@ prompt_and_setup_ssl() {
local ssl_choice=""
echo -e "${yellow}Choose SSL certificate setup method:${plain}"
echo -e "${green}1.${plain} Let's Encrypt (domain required, recommended)"
echo -e "${green}2.${plain} Self-signed certificate (for testing/local use)"
read -rp "Choose an option (default 2): " ssl_choice
echo -e "${green}1.${plain} Let's Encrypt for Domain (90-day validity, auto-renews)"
echo -e "${green}2.${plain} Let's Encrypt for IP Address (6-day validity, auto-renews)"
echo -e "${green}3.${plain} Custom SSL Certificate (Path to existing files)"
echo -e "${blue}Note:${plain} Options 1 & 2 require port 80 open. Option 3 requires manual paths."
read -rp "Choose an option (default 2 for IP): " ssl_choice
ssl_choice="${ssl_choice// /}" # Trim whitespace
# Default to 2 (self-signed) if not 1
if [[ "$ssl_choice" != "1" ]]; then
# Default to 2 (IP cert) if input is empty or invalid (not 1 or 3)
if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" ]]; then
ssl_choice="2"
fi
case "$ssl_choice" in
1)
# 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
# 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}')
@@ -446,32 +567,97 @@ prompt_and_setup_ssl() {
fi
;;
2)
# User chose self-signed option
# Stop panel if running
# User chose Let's Encrypt IP certificate option
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
rc-service x-ui stop >/dev/null 2>&1
else
systemctl stop x-ui >/dev/null 2>&1
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
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
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}"
fi
# Start panel after SSL is configured
# Restart panel after SSL is configured (restart applies new cert settings)
if [[ $release == "alpine" ]]; then
rc-service x-ui start >/dev/null 2>&1
rc-service x-ui restart >/dev/null 2>&1
else
systemctl start x-ui >/dev/null 2>&1
systemctl restart x-ui >/dev/null 2>&1
fi
;;
0)
echo -e "${yellow}Skipping SSL setup${plain}"
SSL_HOST="${server_ip}"
3)
# User chose Custom Paths (User Provided) option
echo -e "${green}Using custom existing certificate...${plain}"
local custom_cert=""
local custom_key=""
local custom_domain=""
# 3.1 Request Domain to compose Panel URL later
read -rp "Please enter domain name certificate issued for: " custom_domain
custom_domain="${custom_domain// /}" # Убираем пробелы
# 3.2 Loop for Certificate Path
while true; do
read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert
# Strip quotes if present
custom_cert=$(echo "$custom_cert" | tr -d '"' | tr -d "'")
if [[ -f "$custom_cert" && -r "$custom_cert" && -s "$custom_cert" ]]; then
break
elif [[ ! -f "$custom_cert" ]]; then
echo -e "${red}Error: File does not exist! Try again.${plain}"
elif [[ ! -r "$custom_cert" ]]; then
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
else
echo -e "${red}Error: File is empty!${plain}"
fi
done
# 3.3 Loop for Private Key Path
while true; do
read -rp "Input private key path (keywords: .key / privatekey): " custom_key
# Strip quotes if present
custom_key=$(echo "$custom_key" | tr -d '"' | tr -d "'")
if [[ -f "$custom_key" && -r "$custom_key" && -s "$custom_key" ]]; then
break
elif [[ ! -f "$custom_key" ]]; then
echo -e "${red}Error: File does not exist! Try again.${plain}"
elif [[ ! -r "$custom_key" ]]; then
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
else
echo -e "${red}Error: File is empty!${plain}"
fi
done
# 3.4 Apply Settings via x-ui binary
${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" >/dev/null 2>&1
# Set SSL_HOST for composing Panel URL
if [[ -n "$custom_domain" ]]; then
SSL_HOST="$custom_domain"
else
SSL_HOST="${server_ip}"
fi
echo -e "${green}✓ Custom certificate paths applied.${plain}"
echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}"
systemctl restart x-ui >/dev/null 2>&1 || rc-service x-ui restart >/dev/null 2>&1
;;
*)
echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
@@ -523,7 +709,7 @@ config_after_update() {
echo -e "${red} ⚠ NO SSL CERTIFICATE DETECTED ⚠ ${plain}"
echo -e "${red}═══════════════════════════════════════════${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 ""
if [[ -z "${server_ip}" ]]; then
@@ -532,7 +718,7 @@ config_after_update() {
return
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}"
echo ""
@@ -576,10 +762,10 @@ update_x-ui() {
fi
fi
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
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
_fail "ERROR: Failed to download x-ui, please be sure that your server can access GitHub"
fi
@@ -613,6 +799,7 @@ update_x-ui() {
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.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 -f >/dev/null 2>&1
rm ${xui_folder}/x-ui.sh -f >/dev/null 2>&1
@@ -695,6 +882,15 @@ update_x-ui() {
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
echo -e "${green}Installing rhel-like systemd unit...${plain}"
@@ -713,6 +909,9 @@ update_x-ui() {
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
;;
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
;;

View File

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

View File

@@ -24,13 +24,22 @@ type Config struct {
// FetchVlessFlags returns map[email]enabled
func FetchVlessFlags(cfg Config) (map[string]bool, error) {
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
var conn *ldap.Conn
var err error
scheme := "ldap"
if cfg.UseTLS {
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false})
} else {
conn, err = ldap.Dial("tcp", addr)
scheme = "ldaps"
}
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 {
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.
func AuthenticateUser(cfg Config, username, password string) (bool, error) {
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
var conn *ldap.Conn
var err error
scheme := "ldap"
if cfg.UseTLS {
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false})
} else {
conn, err = ldap.Dial("tcp", addr)
scheme = "ldaps"
}
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 {
return false, err
}

View File

@@ -18,10 +18,10 @@ var (
// init initializes the character sequences used for random string generation.
// It sets up arrays for numbers, lowercase letters, uppercase letters, and combinations.
func init() {
for i := 0; i < 10; i++ {
for i := range 10 {
numSeq[i] = rune('0' + i)
}
for i := 0; i < 26; i++ {
for i := range 26 {
lowerSeq[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).
func Seq(n int) string {
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))))
if err != nil {
panic("crypto/rand failed: " + err.Error())

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ const Protocols = {
MIXED: 'mixed',
HTTP: 'http',
WIREGUARD: 'wireguard',
TUN: 'tun',
};
const SSMethods = {
@@ -318,14 +319,12 @@ TcpStreamSettings.TcpResponse = class extends XrayCommonClass {
class KcpStreamSettings extends XrayCommonClass {
constructor(
mtu = 1350,
tti = 50,
tti = 20,
uplinkCapacity = 5,
downlinkCapacity = 20,
congestion = false,
readBufferSize = 2,
writeBufferSize = 2,
type = 'none',
seed = RandomUtil.randomSeq(10),
readBufferSize = 1,
writeBufferSize = 1,
) {
super();
this.mtu = mtu;
@@ -335,8 +334,6 @@ class KcpStreamSettings extends XrayCommonClass {
this.congestion = congestion;
this.readBuffer = readBufferSize;
this.writeBuffer = writeBufferSize;
this.type = type;
this.seed = seed;
}
static fromJson(json = {}) {
@@ -348,8 +345,6 @@ class KcpStreamSettings extends XrayCommonClass {
json.congestion,
json.readBufferSize,
json.writeBufferSize,
ObjectUtil.isEmpty(json.header) ? 'none' : json.header.type,
json.seed,
);
}
@@ -362,10 +357,6 @@ class KcpStreamSettings extends XrayCommonClass {
congestion: this.congestion,
readBufferSize: this.readBuffer,
writeBufferSize: this.writeBuffer,
header: {
type: this.type,
},
seed: this.seed,
};
}
}
@@ -496,6 +487,19 @@ class xHTTPStreamSettings extends XrayCommonClass {
noSSEHeader = false,
xPaddingBytes = "100-1000",
mode = MODE_OPTION.AUTO,
xPaddingObfsMode = false,
xPaddingKey = '',
xPaddingHeader = '',
xPaddingPlacement = '',
xPaddingMethod = '',
uplinkHTTPMethod = '',
sessionPlacement = '',
sessionKey = '',
seqPlacement = '',
seqKey = '',
uplinkDataPlacement = '',
uplinkDataKey = '',
uplinkChunkSize = 0,
) {
super();
this.path = path;
@@ -507,6 +511,19 @@ class xHTTPStreamSettings extends XrayCommonClass {
this.noSSEHeader = noSSEHeader;
this.xPaddingBytes = xPaddingBytes;
this.mode = mode;
this.xPaddingObfsMode = xPaddingObfsMode;
this.xPaddingKey = xPaddingKey;
this.xPaddingHeader = xPaddingHeader;
this.xPaddingPlacement = xPaddingPlacement;
this.xPaddingMethod = xPaddingMethod;
this.uplinkHTTPMethod = uplinkHTTPMethod;
this.sessionPlacement = sessionPlacement;
this.sessionKey = sessionKey;
this.seqPlacement = seqPlacement;
this.seqKey = seqKey;
this.uplinkDataPlacement = uplinkDataPlacement;
this.uplinkDataKey = uplinkDataKey;
this.uplinkChunkSize = uplinkChunkSize;
}
addHeader(name, value) {
@@ -528,6 +545,19 @@ class xHTTPStreamSettings extends XrayCommonClass {
json.noSSEHeader,
json.xPaddingBytes,
json.mode,
json.xPaddingObfsMode,
json.xPaddingKey,
json.xPaddingHeader,
json.xPaddingPlacement,
json.xPaddingMethod,
json.uplinkHTTPMethod,
json.sessionPlacement,
json.sessionKey,
json.seqPlacement,
json.seqKey,
json.uplinkDataPlacement,
json.uplinkDataKey,
json.uplinkChunkSize,
);
}
@@ -542,6 +572,19 @@ class xHTTPStreamSettings extends XrayCommonClass {
noSSEHeader: this.noSSEHeader,
xPaddingBytes: this.xPaddingBytes,
mode: this.mode,
xPaddingObfsMode: this.xPaddingObfsMode,
xPaddingKey: this.xPaddingKey,
xPaddingHeader: this.xPaddingHeader,
xPaddingPlacement: this.xPaddingPlacement,
xPaddingMethod: this.xPaddingMethod,
uplinkHTTPMethod: this.uplinkHTTPMethod,
sessionPlacement: this.sessionPlacement,
sessionKey: this.sessionKey,
seqPlacement: this.seqPlacement,
seqKey: this.seqKey,
uplinkDataPlacement: this.uplinkDataPlacement,
uplinkDataKey: this.uplinkDataKey,
uplinkChunkSize: this.uplinkChunkSize,
};
}
}
@@ -553,7 +596,6 @@ class TlsStreamSettings extends XrayCommonClass {
maxVersion = TLS_VERSION_OPTION.TLS13,
cipherSuites = '',
rejectUnknownSni = false,
verifyPeerCertInNames = ['dns.google', 'cloudflare-dns.com'],
disableSystemRoot = false,
enableSessionResumption = false,
certificates = [new TlsStreamSettings.Cert()],
@@ -568,7 +610,6 @@ class TlsStreamSettings extends XrayCommonClass {
this.maxVersion = maxVersion;
this.cipherSuites = cipherSuites;
this.rejectUnknownSni = rejectUnknownSni;
this.verifyPeerCertInNames = Array.isArray(verifyPeerCertInNames) ? verifyPeerCertInNames.join(",") : verifyPeerCertInNames;
this.disableSystemRoot = disableSystemRoot;
this.enableSessionResumption = enableSessionResumption;
this.certs = certificates;
@@ -602,7 +643,6 @@ class TlsStreamSettings extends XrayCommonClass {
json.maxVersion,
json.cipherSuites,
json.rejectUnknownSni,
json.verifyPeerCertInNames,
json.disableSystemRoot,
json.enableSessionResumption,
certs,
@@ -620,7 +660,6 @@ class TlsStreamSettings extends XrayCommonClass {
maxVersion: this.maxVersion,
cipherSuites: this.cipherSuites,
rejectUnknownSni: this.rejectUnknownSni,
verifyPeerCertInNames: this.verifyPeerCertInNames.split(","),
disableSystemRoot: this.disableSystemRoot,
enableSessionResumption: this.enableSessionResumption,
certificates: TlsStreamSettings.toJsonArray(this.certs),
@@ -928,6 +967,51 @@ class SockoptStreamSettings extends XrayCommonClass {
}
}
class FinalMask extends XrayCommonClass {
constructor(type = 'salamander', settings = {}) {
super();
this.type = type;
this.settings = this._getDefaultSettings(type, settings);
}
_getDefaultSettings(type, settings = {}) {
switch (type) {
case 'salamander':
case 'mkcp-aes128gcm':
return { password: settings.password || '' };
case 'header-dns':
case 'xdns':
return { domain: settings.domain || '' };
case 'mkcp-original':
case 'header-dtls':
case 'header-srtp':
case 'header-utp':
case 'header-wechat':
case 'header-wireguard':
return {};
default:
return settings;
}
}
static fromJson(json = {}) {
return new FinalMask(
json.type || 'salamander',
json.settings || {}
);
}
toJson() {
const result = {
type: this.type
};
if (this.settings && Object.keys(this.settings).length > 0) {
result.settings = this.settings;
}
return result;
}
}
class StreamSettings extends XrayCommonClass {
constructor(network = 'tcp',
security = 'none',
@@ -940,6 +1024,7 @@ class StreamSettings extends XrayCommonClass {
grpcSettings = new GrpcStreamSettings(),
httpupgradeSettings = new HTTPUpgradeStreamSettings(),
xhttpSettings = new xHTTPStreamSettings(),
finalmask = { udp: [] },
sockopt = undefined,
) {
super();
@@ -954,9 +1039,23 @@ class StreamSettings extends XrayCommonClass {
this.grpc = grpcSettings;
this.httpupgrade = httpupgradeSettings;
this.xhttp = xhttpSettings;
this.finalmask = finalmask;
this.sockopt = sockopt;
}
addUdpMask(type = 'salamander') {
if (!this.finalmask.udp) {
this.finalmask.udp = [];
}
this.finalmask.udp.push(new FinalMask(type));
}
delUdpMask(index) {
if (this.finalmask.udp) {
this.finalmask.udp.splice(index, 1);
}
}
get isTls() {
return this.security === "tls";
}
@@ -991,6 +1090,14 @@ class StreamSettings extends XrayCommonClass {
}
static fromJson(json = {}) {
let finalmask = { udp: [] };
if (json.finalmask) {
if (Array.isArray(json.finalmask)) {
finalmask.udp = json.finalmask.map(mask => FinalMask.fromJson(mask));
} else if (json.finalmask.udp) {
finalmask.udp = json.finalmask.udp.map(mask => FinalMask.fromJson(mask));
}
}
return new StreamSettings(
json.network,
json.security,
@@ -1003,6 +1110,7 @@ class StreamSettings extends XrayCommonClass {
GrpcStreamSettings.fromJson(json.grpcSettings),
HTTPUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
xHTTPStreamSettings.fromJson(json.xhttpSettings),
finalmask,
SockoptStreamSettings.fromJson(json.sockopt),
);
}
@@ -1021,6 +1129,9 @@ class StreamSettings extends XrayCommonClass {
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,
xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined,
finalmask: (this.finalmask.udp && this.finalmask.udp.length > 0) ? {
udp: this.finalmask.udp.map(mask => mask.toJson())
} : undefined,
sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
};
}
@@ -1739,6 +1850,7 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.MIXED: return new Inbound.MixedSettings(protocol);
case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
case Protocols.TUN: return new Inbound.TunSettings(protocol);
default: return null;
}
}
@@ -1753,6 +1865,7 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.MIXED: return Inbound.MixedSettings.fromJson(json);
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
case Protocols.TUN: return Inbound.TunSettings.fromJson(json);
default: return null;
}
}
@@ -1944,7 +2057,9 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
json.selectedAuth = this.selectedAuth;
}
if (this.testseed && this.testseed.length >= 4) {
// Only include testseed if at least one client has a flow set
const hasFlow = this.vlesses && this.vlesses.some(vless => vless.flow && vless.flow !== '');
if (hasFlow && this.testseed && this.testseed.length >= 4) {
json.testseed = this.testseed;
}
@@ -2586,3 +2701,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",
Socks: "socks",
HTTP: "http",
Wireguard: "wireguard"
Wireguard: "wireguard",
Hysteria: "hysteria"
};
const SSMethods = {
@@ -165,14 +166,12 @@ class TcpStreamSettings extends CommonClass {
class KcpStreamSettings extends CommonClass {
constructor(
mtu = 1350,
tti = 50,
tti = 20,
uplinkCapacity = 5,
downlinkCapacity = 20,
congestion = false,
readBufferSize = 2,
writeBufferSize = 2,
type = 'none',
seed = '',
readBufferSize = 1,
writeBufferSize = 1,
) {
super();
this.mtu = mtu;
@@ -182,8 +181,6 @@ class KcpStreamSettings extends CommonClass {
this.congestion = congestion;
this.readBuffer = readBufferSize;
this.writeBuffer = writeBufferSize;
this.type = type;
this.seed = seed;
}
static fromJson(json = {}) {
@@ -195,8 +192,6 @@ class KcpStreamSettings extends CommonClass {
json.congestion,
json.readBufferSize,
json.writeBufferSize,
ObjectUtil.isEmpty(json.header) ? 'none' : json.header.type,
json.seed,
);
}
@@ -209,10 +204,6 @@ class KcpStreamSettings extends CommonClass {
congestion: this.congestion,
readBufferSize: this.readBuffer,
writeBufferSize: this.writeBuffer,
header: {
type: this.type,
},
seed: this.seed,
};
}
}
@@ -356,6 +347,8 @@ class TlsStreamSettings extends CommonClass {
fingerprint = '',
allowInsecure = false,
echConfigList = '',
verifyPeerCertByName = 'cloudflare-dns.com',
pinnedPeerCertSha256 = '',
) {
super();
this.serverName = serverName;
@@ -363,6 +356,8 @@ class TlsStreamSettings extends CommonClass {
this.fingerprint = fingerprint;
this.allowInsecure = allowInsecure;
this.echConfigList = echConfigList;
this.verifyPeerCertByName = verifyPeerCertByName;
this.pinnedPeerCertSha256 = pinnedPeerCertSha256;
}
static fromJson(json = {}) {
@@ -372,6 +367,8 @@ class TlsStreamSettings extends CommonClass {
json.fingerprint,
json.allowInsecure,
json.echConfigList,
json.verifyPeerCertByName,
json.pinnedPeerCertSha256,
);
}
@@ -381,7 +378,9 @@ class TlsStreamSettings extends CommonClass {
alpn: this.alpn,
fingerprint: this.fingerprint,
allowInsecure: this.allowInsecure,
echConfigList: this.echConfigList
echConfigList: this.echConfigList,
verifyPeerCertByName: this.verifyPeerCertByName,
pinnedPeerCertSha256: this.pinnedPeerCertSha256
};
}
}
@@ -424,6 +423,102 @@ class RealityStreamSettings extends CommonClass {
};
}
};
class HysteriaStreamSettings extends CommonClass {
constructor(
version = 2,
auth = '',
congestion = '',
up = '0',
down = '0',
udphopPort = '',
udphopIntervalMin = 30,
udphopIntervalMax = 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.udphopIntervalMin = udphopIntervalMin;
this.udphopIntervalMax = udphopIntervalMax;
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 udphopIntervalMin = 30;
let udphopIntervalMax = 30;
if (json.udphop) {
udphopPort = json.udphop.port || '';
// Backward compatibility: if old 'interval' exists, use it for both min/max
if (json.udphop.interval !== undefined) {
udphopIntervalMin = json.udphop.interval;
udphopIntervalMax = json.udphop.interval;
} else {
udphopIntervalMin = json.udphop.intervalMin || 30;
udphopIntervalMax = json.udphop.intervalMax || 30;
}
}
return new HysteriaStreamSettings(
json.version,
json.auth,
json.congestion,
json.up,
json.down,
udphopPort,
udphopIntervalMin,
udphopIntervalMax,
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,
intervalMin: this.udphopIntervalMin,
intervalMax: this.udphopIntervalMax
};
}
return result;
}
};
class SockoptStreamSettings extends CommonClass {
constructor(
dialerProxy = "",
@@ -473,6 +568,52 @@ class SockoptStreamSettings extends CommonClass {
}
}
class FinalMask extends CommonClass {
constructor(type = 'salamander', settings = {}) {
super();
this.type = type;
this.settings = this._getDefaultSettings(type, settings);
}
_getDefaultSettings(type, settings = {}) {
switch (type) {
case 'salamander':
case 'mkcp-aes128gcm':
return { password: settings.password || '' };
case 'header-dns':
case 'xdns':
return { domain: settings.domain || '' };
case 'mkcp-original':
case 'header-dtls':
case 'header-srtp':
case 'header-utp':
case 'header-wechat':
case 'header-wireguard':
return {}; // No settings needed
default:
return settings;
}
}
static fromJson(json = {}) {
return new FinalMask(
json.type || 'salamander',
json.settings || {}
);
}
toJson() {
const result = {
type: this.type
};
// Only include settings if they exist and are not empty
if (this.settings && Object.keys(this.settings).length > 0) {
result.settings = this.settings;
}
return result;
}
}
class StreamSettings extends CommonClass {
constructor(
network = 'tcp',
@@ -485,6 +626,8 @@ class StreamSettings extends CommonClass {
grpcSettings = new GrpcStreamSettings(),
httpupgradeSettings = new HttpUpgradeStreamSettings(),
xhttpSettings = new xHTTPStreamSettings(),
hysteriaSettings = new HysteriaStreamSettings(),
finalmask = { udp: [] },
sockopt = undefined,
) {
super();
@@ -498,9 +641,24 @@ class StreamSettings extends CommonClass {
this.grpc = grpcSettings;
this.httpupgrade = httpupgradeSettings;
this.xhttp = xhttpSettings;
this.hysteria = hysteriaSettings;
this.finalmask = finalmask;
this.sockopt = sockopt;
}
addUdpMask(type = 'salamander') {
if (!this.finalmask.udp) {
this.finalmask.udp = [];
}
this.finalmask.udp.push(new FinalMask(type));
}
delUdpMask(index) {
if (this.finalmask.udp) {
this.finalmask.udp.splice(index, 1);
}
}
get isTls() {
return this.security === 'tls';
}
@@ -518,6 +676,16 @@ class StreamSettings extends CommonClass {
}
static fromJson(json = {}) {
let finalmask = { udp: [] };
if (json.finalmask) {
if (Array.isArray(json.finalmask)) {
// Legacy format: direct array (backward compatibility)
finalmask.udp = json.finalmask.map(mask => FinalMask.fromJson(mask));
} else if (json.finalmask.udp) {
// New format: object with udp array
finalmask.udp = json.finalmask.udp.map(mask => FinalMask.fromJson(mask));
}
}
return new StreamSettings(
json.network,
json.security,
@@ -529,6 +697,8 @@ class StreamSettings extends CommonClass {
GrpcStreamSettings.fromJson(json.grpcSettings),
HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
xHTTPStreamSettings.fromJson(json.xhttpSettings),
HysteriaStreamSettings.fromJson(json.hysteriaSettings),
finalmask,
SockoptStreamSettings.fromJson(json.sockopt),
);
}
@@ -546,6 +716,10 @@ class StreamSettings extends CommonClass {
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,
xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined,
hysteriaSettings: network === 'hysteria' ? this.hysteria.toJson() : undefined,
finalmask: (this.finalmask.udp && this.finalmask.udp.length > 0) ? {
udp: this.finalmask.udp.map(mask => mask.toJson())
} : undefined,
sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
};
}
@@ -609,7 +783,8 @@ class Outbound extends CommonClass {
}
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);
}
@@ -634,7 +809,7 @@ class Outbound extends CommonClass {
}
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() {
@@ -673,7 +848,8 @@ class Outbound extends CommonClass {
Protocols.Trojan,
Protocols.Shadowsocks,
Protocols.Socks,
Protocols.HTTP
Protocols.HTTP,
Protocols.Hysteria
].includes(this.protocol);
}
@@ -722,6 +898,9 @@ class Outbound extends CommonClass {
case Protocols.Trojan:
case 'ss':
return this.fromParamLink(link);
case 'hysteria2':
case Protocols.Hysteria:
return this.fromHysteriaLink(link);
default:
return null;
}
@@ -842,6 +1021,70 @@ class Outbound extends CommonClass {
remark = remark.length > 0 ? remark.substring(1) : 'out-' + protocol + '-' + port;
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') ?? '';
// Support both old single interval and new min/max range
if (urlParams.has('udphopInterval')) {
const interval = parseInt(urlParams.get('udphopInterval'));
stream.hysteria.udphopIntervalMin = interval;
stream.hysteria.udphopIntervalMax = interval;
} else {
stream.hysteria.udphopIntervalMin = parseInt(urlParams.get('udphopIntervalMin') ?? '30');
stream.hysteria.udphopIntervalMax = parseInt(urlParams.get('udphopIntervalMax') ?? '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 {
@@ -862,6 +1105,7 @@ Outbound.Settings = class extends CommonClass {
case Protocols.Socks: return new Outbound.SocksSettings();
case Protocols.HTTP: return new Outbound.HttpSettings();
case Protocols.Wireguard: return new Outbound.WireguardSettings();
case Protocols.Hysteria: return new Outbound.HysteriaSettings();
default: return null;
}
}
@@ -878,6 +1122,7 @@ Outbound.Settings = class extends CommonClass {
case Protocols.Socks: return Outbound.SocksSettings.fromJson(json);
case Protocols.HTTP: return Outbound.HttpSettings.fromJson(json);
case Protocols.Wireguard: return Outbound.WireguardSettings.fromJson(json);
case Protocols.Hysteria: return Outbound.HysteriaSettings.fromJson(json);
default: return null;
}
}
@@ -1096,11 +1341,14 @@ Outbound.VLESSSettings = class extends CommonClass {
flow: this.flow,
encryption: this.encryption,
};
if (this.testpre > 0) {
result.testpre = this.testpre;
}
if (this.testseed && this.testseed.length >= 4) {
result.testseed = this.testseed;
// Only include Vision settings when flow is set
if (this.flow && this.flow !== '') {
if (this.testpre > 0) {
result.testpre = this.testpre;
}
if (this.testseed && this.testseed.length >= 4) {
result.testseed = this.testseed;
}
}
return result;
}
@@ -1324,4 +1572,30 @@ Outbound.WireguardSettings.Peer = class extends CommonClass {
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

@@ -1,18 +1,15 @@
// List of popular services for VLESS Reality Target/SNI randomization
const REALITY_TARGETS = [
{ target: 'www.icloud.com:443', sni: 'www.icloud.com,icloud.com' },
{ target: 'www.apple.com:443', sni: 'www.apple.com,apple.com' },
{ target: 'www.tesla.com:443', sni: 'www.tesla.com,tesla.com' },
{ target: 'www.sony.com:443', sni: 'www.sony.com,sony.com' },
{ target: 'www.nvidia.com:443', sni: 'www.nvidia.com,nvidia.com' },
{ target: 'www.amd.com:443', sni: 'www.amd.com,amd.com' },
{ target: 'azure.microsoft.com:443', sni: 'azure.microsoft.com,www.azure.com' },
{ target: 'aws.amazon.com:443', sni: 'aws.amazon.com,amazon.com' },
{ target: 'www.bing.com:443', sni: 'www.bing.com,bing.com' },
{ target: 'www.oracle.com:443', sni: 'www.oracle.com,oracle.com' },
{ target: 'www.intel.com:443', sni: 'www.intel.com,intel.com' },
{ target: 'www.microsoft.com:443', sni: 'www.microsoft.com,microsoft.com' },
{ target: 'www.amazon.com:443', sni: 'www.amazon.com,amazon.com' }
{ target: 'www.apple.com:443', sni: 'www.apple.com' },
{ target: 'www.icloud.com:443', sni: 'www.icloud.com' },
{ target: 'www.amazon.com:443', sni: 'www.amazon.com' },
{ target: 'aws.amazon.com:443', sni: 'aws.amazon.com' },
{ target: 'www.oracle.com:443', sni: 'www.oracle.com' },
{ target: 'www.nvidia.com:443', sni: 'www.nvidia.com' },
{ target: 'www.amd.com:443', sni: 'www.amd.com' },
{ target: 'www.intel.com:443', sni: 'www.intel.com' },
{ target: 'www.tesla.com:443', sni: 'www.tesla.com' },
{ target: 'www.sony.com:443', sni: 'www.sony.com' }
];
/**
@@ -28,4 +25,3 @@ function getRandomRealityTarget() {
sni: selected.sni
};
}

View File

@@ -29,6 +29,11 @@ class AllSetting {
this.subEnable = true;
this.subJsonEnable = false;
this.subTitle = "";
this.subSupportUrl = "";
this.subProfileUrl = "";
this.subAnnounce = "";
this.subEnableRouting = true;
this.subRoutingRules = "";
this.subListen = "";
this.subPort = 2096;
this.subPath = "/sub/";

View File

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

View File

@@ -142,7 +142,7 @@ class RandomUtil {
let length = 32;
if ([SSMethods.BLAKE3_AES_128_GCM].includes(method)) {
length = 16;
length = 16;
}
const array = new Uint8Array(length);
@@ -154,28 +154,28 @@ class RandomUtil {
static randomBase32String(length = 16) {
const array = new Uint8Array(length);
window.crypto.getRandomValues(array);
const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
let result = '';
let bits = 0;
let buffer = 0;
for (let i = 0; i < array.length; i++) {
buffer = (buffer << 8) | array[i];
bits += 8;
while (bits >= 5) {
bits -= 5;
result += base32Chars[(buffer >>> bits) & 0x1F];
}
}
if (bits > 0) {
result += base32Chars[(buffer << (5 - bits)) & 0x1F];
}
return result;
}
}
@@ -908,7 +908,10 @@ class IntlUtil {
const language = LanguageManager.getLanguage()
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' })
return formatter.format(diff, 'day');

View File

@@ -14,10 +14,12 @@ class WebSocketClient {
}
connect() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
return;
}
this.shouldReconnect = true;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// Ensure basePath ends with '/' for proper URL construction
let basePath = this.basePath || '';
@@ -97,7 +99,10 @@ class WebSocketClient {
if (!this.listeners.has(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) {

View File

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

View File

@@ -57,6 +57,11 @@ type AllSetting struct {
SubEnable bool `json:"subEnable" form:"subEnable"` // Enable subscription server
SubJsonEnable bool `json:"subJsonEnable" form:"subJsonEnable"` // Enable JSON subscription endpoint
SubTitle string `json:"subTitle" form:"subTitle"` // Subscription title
SubSupportUrl string `json:"subSupportUrl" form:"subSupportUrl"` // Subscription support URL
SubProfileUrl string `json:"subProfileUrl" form:"subProfileUrl"` // Subscription profile URL
SubAnnounce string `json:"subAnnounce" form:"subAnnounce"` // Subscription announce
SubEnableRouting bool `json:"subEnableRouting" form:"subEnableRouting"` // Enable routing for subscription
SubRoutingRules string `json:"subRoutingRules" form:"subRoutingRules"` // Subscription global routing rules (Only for Happ)
SubListen string `json:"subListen" form:"subListen"` // Subscription server listen IP
SubPort int `json:"subPort" form:"subPort"` // Subscription server port
SubPath string `json:"subPath" form:"subPath"` // Base path for subscription URLs

View File

@@ -17,7 +17,7 @@ var (
type WebServer interface {
GetCron() *cron.Cron // Get the cron scheduler
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.

View File

@@ -24,6 +24,40 @@
body {
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>
<title>{{ .host }} {{ i18n .title}}</title>
{{ end }}

View File

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

View File

@@ -1,12 +1,16 @@
{{define "form/outbound"}}
<!-- 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'); }">
<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-select v-model="outbound.protocol" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x,y in Protocols" :value="x">[[ y ]]</a-select-option>
<a-select v-model="outbound.protocol"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x,y in Protocols" :value="x">[[ y
]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.outbound.tag" }}' has-feedback
@@ -21,8 +25,10 @@
<!-- freedom settings-->
<template v-if="outbound.protocol === Protocols.Freedom">
<a-form-item label='Strategy'>
<a-select v-model="outbound.settings.domainStrategy" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in OutboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
<a-select v-model="outbound.settings.domainStrategy"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in OutboundDomainStrategies" :value="s">[[
s ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Redirect'>
@@ -35,18 +41,22 @@
</a-form-item>
<template v-if="Object.keys(outbound.settings.fragment).length >0">
<a-form-item label='Packets'>
<a-select v-model="outbound.settings.fragment.packets" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['1-3','tlshello']" :value="s">[[ s ]]</a-select-option>
<a-select v-model="outbound.settings.fragment.packets"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['1-3','tlshello']" :value="s">[[ s
]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Length'>
<a-input v-model.trim="outbound.settings.fragment.length"></a-input>
</a-form-item>
<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 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>
</template>
@@ -60,11 +70,13 @@
<!-- Add Noise Button -->
<template v-if="outbound.settings.noises.length > 0">
<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>
<!-- 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} }">
<a-divider :style="{ margin: '0' }"> Noise [[ index + 1 ]]
<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>
</a-divider>
<a-form-item label='Type'>
<a-select v-model="noise.type" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['rand','base64','str', 'hex']" :value="s">[[ s ]]</a-select-option>
<a-select v-model="noise.type"
: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-form-item>
<a-form-item label='Packet'>
@@ -83,8 +97,10 @@
<a-input v-model.trim="noise.delay"></a-input>
</a-form-item>
<a-form-item label='Apply To'>
<a-select v-model="noise.applyTo" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['ip','ipv4','ipv6']" :value="s">[[ s ]]</a-select-option>
<a-select v-model="noise.applyTo"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['ip','ipv4','ipv6']" :value="s">[[
s ]]</a-select-option>
</a-select>
</a-form-item>
</a-form>
@@ -94,8 +110,10 @@
<!-- blackhole settings -->
<template v-if="outbound.protocol === Protocols.Blackhole">
<a-form-item label='Response Type'>
<a-select v-model="outbound.settings.type" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['', 'none','http']" :value="s">[[ s ]]</a-select-option>
<a-select v-model="outbound.settings.type"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['', 'none','http']" :value="s">[[ s
]]</a-select-option>
</a-select>
</a-form-item>
</template>
@@ -103,16 +121,21 @@
<!-- dns settings -->
<template v-if="outbound.protocol === Protocols.DNS">
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
<a-select v-model="outbound.settings.network" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['udp','tcp']" :value="s">[[ s ]]</a-select-option>
<a-select v-model="outbound.settings.network"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['udp','tcp']" :value="s">[[ s
]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='non-IP queries'>
<a-select v-model="outbound.settings.nonIPQuery" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['reject','drop','skip']" :value="s">[[ s ]]</a-select-option>
<a-select v-model="outbound.settings.nonIPQuery"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['reject','drop','skip']" :value="s">[[
s ]]</a-select-option>
</a-select>
</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-form-item>
</template>
@@ -149,15 +172,19 @@
<a-input disabled v-model="outbound.settings.pubKey"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.wireguard.domainStrategy" }}'>
<a-select v-model="outbound.settings.domainStrategy" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="wds in ['', ...WireguardDomainStrategy]" :value="wds">[[ wds ]]</a-select-option>
<a-select v-model="outbound.settings.domainStrategy"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="wds in ['', ...WireguardDomainStrategy]"
:value="wds">[[ wds ]]</a-select-option>
</a-select>
</a-form-item>
<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 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 label='No Kernel Tun'>
<a-switch v-model="outbound.settings.noKernelTun"></a-switch>
@@ -173,11 +200,14 @@
<a-input v-model="outbound.settings.reserved"></a-input>
</a-form-item>
<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 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} }">
<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)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider>
@@ -193,17 +223,21 @@
<a-form-item>
<template slot="label">
{{ 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 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-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>
</a-input>
</template>
</a-form-item>
<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>
</template>
@@ -214,12 +248,14 @@
<a-input v-model.trim="outbound.settings.address"></a-input>
</a-form-item>
<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>
</template>
<!-- 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-input v-model.trim="outbound.settings.id"></a-input>
</a-form-item>
@@ -227,8 +263,10 @@
<!-- vmess settings -->
<template v-if="outbound.protocol === Protocols.VMess">
<a-form-item label='Security'>
<a-select v-model="outbound.settings.security" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
<a-select v-model="outbound.settings.security"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key
]]</a-select-option>
</a-select>
</a-form-item>
</template>
@@ -241,35 +279,47 @@
</template>
<template v-if="outbound.canEnableTlsFlow()">
<a-form-item label='Flow'>
<a-select v-model="outbound.settings.flow" :dropdown-class-name="themeSwitcher.currentTheme">
<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 v-model="outbound.settings.flow"
:dropdown-class-name="themeSwitcher.currentTheme">
<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-form-item>
</template>
<!-- XTLS Vision Advanced Settings -->
<template v-if="outbound.canEnableVisionSeed()">
<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>
</a-form-item>
<a-form-item label="Vision Seed">
<a-row :gutter="8">
<a-col :span="6">
<a-input-number v-model.number="outbound.settings.testseed[0]" :min="0" :max="9999"
:style="{ width: '100%' }" placeholder="900" addon-before="[0]"></a-input-number>
<a-input-number v-model.number="outbound.settings.testseed[0]"
:min="0" :max="9999"
:style="{ width: '100%' }" placeholder="900"
addon-before="[0]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number v-model.number="outbound.settings.testseed[1]" :min="0" :max="9999"
:style="{ width: '100%' }" placeholder="500" addon-before="[1]"></a-input-number>
<a-input-number v-model.number="outbound.settings.testseed[1]"
:min="0" :max="9999"
:style="{ width: '100%' }" placeholder="500"
addon-before="[1]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number v-model.number="outbound.settings.testseed[2]" :min="0" :max="9999"
:style="{ width: '100%' }" placeholder="900" addon-before="[2]"></a-input-number>
<a-input-number v-model.number="outbound.settings.testseed[2]"
:min="0" :max="9999"
:style="{ width: '100%' }" placeholder="900"
addon-before="[2]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number v-model.number="outbound.settings.testseed[3]" :min="0" :max="9999"
:style="{ width: '100%' }" placeholder="256" addon-before="[3]"></a-input-number>
<a-input-number v-model.number="outbound.settings.testseed[3]"
:min="0" :max="9999"
:style="{ width: '100%' }" placeholder="256"
addon-before="[3]"></a-input-number>
</a-col>
</a-row>
</a-form-item>
@@ -289,7 +339,8 @@
</template>
<!-- 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-input v-model.trim="outbound.settings.password"></a-input>
</a-form-item>
@@ -298,8 +349,10 @@
<!-- shadowsocks -->
<template v-if="outbound.protocol === Protocols.Shadowsocks">
<a-form-item label='{{ i18n "encryption" }}'>
<a-select v-model="outbound.settings.method" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="(method, method_name) in SSMethods" :value="method">[[ method_name
<a-select v-model="outbound.settings.method"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="(method, method_name) in SSMethods"
:value="method">[[ method_name
]]</a-select-option>
</a-select>
</a-form-item>
@@ -307,15 +360,25 @@
<a-switch v-model="outbound.settings.uot"></a-switch>
</a-form-item>
<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>
</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 -->
<template v-if="outbound.canEnableStream()">
<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">
<a-select-option value="tcp">TCP (RAW)</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="httpupgrade">HTTPUpgrade</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-form-item>
<template v-if="outbound.stream.network === 'tcp'">
@@ -342,40 +407,32 @@
<!-- kcp -->
<template v-if="outbound.stream.network === 'kcp'">
<a-form-item label='{{ i18n "camouflage" }}'>
<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="srtp">SRTP</a-select-option>
<a-select-option value="utp">uTP</a-select-option>
<a-select-option value="wechat-video">WeChat</a-select-option>
<a-select-option value="dtls">DTLS 1.2</a-select-option>
<a-select-option value="wireguard">WireGuard</a-select-option>
<a-select-option value="dns">DNS</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model="outbound.stream.kcp.seed"></a-input>
</a-form-item>
<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 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 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 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 label='Congestion'>
<a-switch v-model="outbound.stream.kcp.congestion"></a-switch>
</a-form-item>
<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 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>
</template>
@@ -388,7 +445,8 @@
<a-input v-model.trim="outbound.stream.ws.path"></a-input>
</a-form-item>
<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>
</template>
@@ -424,45 +482,199 @@
<a-input v-model.trim="outbound.stream.xhttp.path"></a-input>
</a-form-item>
<a-form-item label='Mode'>
<a-select v-model="outbound.stream.xhttp.mode" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in MODE_OPTION" :value="key">[[ key ]]</a-select-option>
<a-select v-model="outbound.stream.xhttp.mode"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in MODE_OPTION" :value="key">[[ key
]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="No gRPC Header"
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-form-item>
<a-form-item label="Min Upload Interval (Ms)" v-if="outbound.stream.xhttp.mode === 'packet-up'">
<a-input v-model.trim="outbound.stream.xhttp.scMinPostsIntervalMs"></a-input>
<a-form-item label="Min Upload Interval (Ms)"
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 label="Max Concurrency" v-if="!outbound.stream.xhttp.xmux.maxConnections">
<a-input v-model="outbound.stream.xhttp.xmux.maxConcurrency"></a-input>
<a-form-item label="Max Concurrency"
v-if="!outbound.stream.xhttp.xmux.maxConnections">
<a-input
v-model="outbound.stream.xhttp.xmux.maxConcurrency"></a-input>
</a-form-item>
<a-form-item label="Max Connections" v-if="!outbound.stream.xhttp.xmux.maxConcurrency">
<a-input v-model="outbound.stream.xhttp.xmux.maxConnections"></a-input>
<a-form-item label="Max Connections"
v-if="!outbound.stream.xhttp.xmux.maxConcurrency">
<a-input
v-model="outbound.stream.xhttp.xmux.maxConnections"></a-input>
</a-form-item>
<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 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 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 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>
</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 Min (s)'
v-if="outbound.stream.hysteria.udphopPort">
<a-input-number
v-model.number="outbound.stream.hysteria.udphopIntervalMin"
:min="5"></a-input-number>
</a-form-item>
<a-form-item label='UDP Hop Interval Max (s)'
v-if="outbound.stream.hysteria.udphopPort">
<a-input-number
v-model.number="outbound.stream.hysteria.udphopIntervalMax"
: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>
<!-- finalmask settings -->
<template v-if="outbound.canEnableStream()">
<a-form-item label="UDP Masks">
<a-button icon="plus" type="primary" size="small"
@click="outbound.stream.addUdpMask(outbound.protocol === Protocols.Hysteria ? 'salamander' : (outbound.stream.network === 'kcp' ? 'mkcp-aes128gcm' : 'xdns'))"></a-button>
</a-form-item>
<template
v-if="outbound.stream.finalmask.udp && outbound.stream.finalmask.udp.length > 0">
<a-form v-for="(mask, index) in outbound.stream.finalmask.udp"
: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"
@change="(type) => mask.settings = mask._getDefaultSettings(type, {})"
:dropdown-class-name="themeSwitcher.currentTheme">
<!-- Salamander for Hysteria2 only -->
<a-select-option v-if="outbound.protocol === Protocols.Hysteria"
value="salamander">
Salamander (Hysteria2)</a-select-option>
<!-- mKCP-specific masks -->
<a-select-option v-if="outbound.stream.network === 'kcp'"
value="mkcp-aes128gcm">
mKCP AES-128-GCM</a-select-option>
<a-select-option v-if="outbound.stream.network === 'kcp'"
value="header-dns">
Header DNS</a-select-option>
<a-select-option v-if="outbound.stream.network === 'kcp'"
value="header-dtls">
Header DTLS 1.2</a-select-option>
<a-select-option v-if="outbound.stream.network === 'kcp'"
value="header-srtp">
Header SRTP</a-select-option>
<a-select-option v-if="outbound.stream.network === 'kcp'"
value="header-utp">
Header uTP</a-select-option>
<a-select-option v-if="outbound.stream.network === 'kcp'"
value="header-wechat">
Header WeChat Video</a-select-option>
<a-select-option v-if="outbound.stream.network === 'kcp'"
value="header-wireguard">
Header WireGuard</a-select-option>
<a-select-option v-if="outbound.stream.network === 'kcp'"
value="mkcp-original">
mKCP Original</a-select-option>
<!-- xDNS for TCP/WS/HTTPUpgrade/XHTTP -->
<a-select-option
v-if="['tcp', 'ws', 'httpupgrade', 'xhttp'].includes(outbound.stream.network)"
value="xdns">
xDNS (Experimental)</a-select-option>
</a-select>
</a-form-item>
<!-- Settings for password-based masks -->
<a-form-item label='Password'
v-if="['salamander', 'mkcp-aes128gcm'].includes(mask.type)">
<a-input v-model.trim="mask.settings.password"
placeholder="Obfuscation password"></a-input>
</a-form-item>
<!-- Settings for domain-based masks -->
<a-form-item label='Domain'
v-if="['header-dns', 'xdns'].includes(mask.type)">
<a-input v-model.trim="mask.settings.domain"
placeholder="e.g., www.example.com"></a-input>
</a-form-item>
</a-form>
</template>
</template>
<!-- tls settings -->
<template v-if="outbound.canEnableTls()">
<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="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-form-item>
<template v-if="outbound.stream.isTls">
@@ -470,15 +682,19 @@
<a-input v-model.trim="outbound.stream.tls.serverName"></a-input>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="outbound.stream.tls.fingerprint" :dropdown-class-name="themeSwitcher.currentTheme">
<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 v-model="outbound.stream.tls.fingerprint"
:dropdown-class-name="themeSwitcher.currentTheme">
<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-form-item>
<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">
<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-form-item>
<a-form-item label="ECH Config List">
@@ -487,16 +703,28 @@
<a-form-item label="Allow Insecure">
<a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch>
</a-form-item>
<a-form-item label="verify Peer Cert By Name">
<a-input
v-model.trim="outbound.stream.tls.verifyPeerCertByName"></a-input>
</a-form-item>
<a-form-item label="pinned Peer Cert Sha256">
<a-input v-model.trim="outbound.stream.tls.pinnedPeerCertSha256"
placeholder="Enter SHA256 fingerprints (base64)">
</a-input>
</a-form-item>
</template>
<!-- reality settings -->
<template v-if="outbound.stream.isReality">
<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 label="uTLS">
<a-select v-model="outbound.stream.reality.fingerprint" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
<a-select v-model="outbound.stream.reality.fingerprint"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[
key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Short ID">
@@ -506,10 +734,12 @@
<a-input v-model.trim="outbound.stream.reality.spiderX"></a-input>
</a-form-item>
<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 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>
</template>
</template>
@@ -520,18 +750,23 @@
</a-form-item>
<template v-if="outbound.stream.sockoptSwitch">
<a-form-item label="Dialer Proxy">
<a-select v-model="outbound.stream.sockopt.dialerProxy" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="tag in ['', ...outModal.tags]" :value="tag">[[ tag ]]</a-select-option>
<a-select v-model="outbound.stream.sockopt.dialerProxy"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="tag in ['', ...outModal.tags]"
:value="tag">[[ tag ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Address Port Strategy'>
<a-select v-model="outbound.stream.sockopt.addressPortStrategy"
: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-form-item>
<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 label="TCP Fast Open">
<a-switch v-model="outbound.stream.sockopt.tcpFastOpen"></a-switch>
@@ -543,11 +778,15 @@
<a-switch v-model="outbound.stream.sockopt.penetrate"></a-switch>
</a-form-item>
<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">
<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="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>
</a-form-item>
@@ -560,14 +799,18 @@
</a-form-item>
<template v-if="outbound.mux.enabled">
<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 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 label="xudp UDP 443">
<a-select v-model="outbound.mux.xudpProxyUDP443" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="c in ['reject', 'allow', 'skip']" :value="c">[[ c ]]</a-select-option>
<a-select v-model="outbound.mux.xudpProxyUDP443"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="c in ['reject', 'allow', 'skip']"
:value="c">[[ c ]]</a-select-option>
</a-select>
</a-form-item>
</template>
@@ -576,11 +819,13 @@
</a-tab-pane>
<a-tab-pane key="2" tab="JSON" force-render="true">
<a-space direction="vertical" :size="10" :style="{ marginTop: '10px' }">
<a-input addon-before='{{ i18n "pages.xray.outbound.link" }}' v-model.trim="outModal.link"
placeholder="vmess:// vless:// trojan:// ss://">
<a-input addon-before='{{ i18n "pages.xray.outbound.link" }}'
v-model.trim="outModal.link"
placeholder="vmess:// vless:// trojan:// ss:// hysteria2://">
<a-icon slot="addonAfter" type="form" @click="convertLink"></a-icon>
</a-input>
<textarea :style="{ position: 'absolute', left: '-800px' }" id="outboundJson"></textarea>
<textarea :style="{ position: 'absolute', left: '-800px' }"
id="outboundJson"></textarea>
</a-space>
</a-tab-pane>
</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"}}
<a-collapse activeKey="0"
v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
{{template "form/client"}}
</a-collapse-panel>
@@ -22,115 +21,103 @@
</a-collapse-panel>
</a-collapse>
<template v-if=" !inbound.stream.isTLS || !inbound.stream.isReality">
<a-form :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-form-item label="Authentication">
<a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="X25519, not Post-Quantum">X25519 (not
Post-Quantum)</a-select-option>
<a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768
(Post-Quantum)</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="decryption">
<a-input v-model.trim="inbound.settings.decryption"></a-input>
</a-form-item>
<a-form-item label="encryption">
<a-input v-model="inbound.settings.encryption"></a-input>
</a-form-item>
<a-form-item label=" ">
<a-space>
<a-button type="primary" icon="import" @click="getNewVlessEnc">Get New
keys</a-button>
<a-button danger @click="clearVlessEnc">Clear</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider v-if="inbound.settings.selectedAuth"
:style="{ margin: '5px 0' }"></a-divider>
</template>
<template v-if="inbound.isTcp && !inbound.settings.selectedAuth">
<a-form :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks">
<a-button icon="plus" type="primary" size="small"
@click="inbound.settings.addFallback()"></a-button>
</a-form-item>
</a-form>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Authentication">
<a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="X25519, not Post-Quantum">X25519 (not
Post-Quantum)</a-select-option>
<a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768
(Post-Quantum)</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="decryption">
<a-input v-model.trim="inbound.settings.decryption"></a-input>
</a-form-item>
<a-form-item label="encryption">
<a-input v-model="inbound.settings.encryption"></a-input>
</a-form-item>
<a-form-item label=" ">
<a-space>
<a-button type="primary" icon="import" @click="getNewVlessEnc">Get New
keys</a-button>
<a-button danger @click="clearVlessEnc">Clear</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider :style="{ margin: '5px 0' }"></a-divider>
</template>
<template v-if="inbound.isTcp && !inbound.settings.selectedAuth">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks">
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
</a-form-item>
</a-form>
<!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon
type="delete"
@click="() => inbound.settings.delFallback(index)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider>
<a-form-item label='SNI'>
<a-input v-model="fallback.name"></a-input>
</a-form-item>
<a-form-item label='ALPN'>
<a-input v-model="fallback.alpn"></a-input>
</a-form-item>
<a-form-item label='Path'>
<a-input v-model="fallback.path"></a-input>
</a-form-item>
<a-form-item label='Dest'>
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label='xVer'>
<a-input-number v-model.number="fallback.xver" :min="0"
:max="2"></a-input-number>
</a-form-item>
</a-form>
<a-divider :style="{ margin: '5px 0' }"></a-divider>
</template>
<template v-if="inbound.canEnableVisionSeed()">
<a-form :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-form-item label="Vision Seed">
<a-row :gutter="8">
<a-col :span="6">
<a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900"
@change="(val) => updateTestseed(0, val)" :min="0" :max="9999"
:style="{ width: '100%' }"
placeholder="900" addon-before="[0]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500"
@change="(val) => updateTestseed(1, val)" :min="0" :max="9999"
:style="{ width: '100%' }"
placeholder="500" addon-before="[1]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900"
@change="(val) => updateTestseed(2, val)" :min="0" :max="9999"
:style="{ width: '100%' }"
placeholder="900" addon-before="[2]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256"
@change="(val) => updateTestseed(3, val)" :min="0" :max="9999"
:style="{ width: '100%' }"
placeholder="256" addon-before="[3]"></a-input-number>
</a-col>
</a-row>
<a-space :size="8" :style="{ marginTop: '8px' }">
<a-button type="primary" @click="setRandomTestseed">
Rand
</a-button>
<a-button @click="resetTestseed">
Reset
</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider :style="{ margin: '5px 0' }"></a-divider>
</template>
<!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete"
@click="() => inbound.settings.delFallback(index)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider>
<a-form-item label='SNI'>
<a-input v-model="fallback.name"></a-input>
</a-form-item>
<a-form-item label='ALPN'>
<a-input v-model="fallback.alpn"></a-input>
</a-form-item>
<a-form-item label='Path'>
<a-input v-model="fallback.path"></a-input>
</a-form-item>
<a-form-item label='Dest'>
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label='xVer'>
<a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
</a-form-item>
</a-form>
<a-divider :style="{ margin: '5px 0' }"></a-divider>
</template>
<template v-if="inbound.canEnableVisionSeed()">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Vision Seed">
<a-row :gutter="8">
<a-col :span="6">
<a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900"
@change="(val) => updateTestseed(0, val)" :min="0" :max="9999" :style="{ width: '100%' }"
placeholder="900" addon-before="[0]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500"
@change="(val) => updateTestseed(1, val)" :min="0" :max="9999" :style="{ width: '100%' }"
placeholder="500" addon-before="[1]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900"
@change="(val) => updateTestseed(2, val)" :min="0" :max="9999" :style="{ width: '100%' }"
placeholder="900" addon-before="[2]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256"
@change="(val) => updateTestseed(3, val)" :min="0" :max="9999" :style="{ width: '100%' }"
placeholder="256" addon-before="[3]"></a-input-number>
</a-col>
</a-row>
<a-space :size="8" :style="{ marginTop: '8px' }">
<a-button type="primary" @click="setRandomTestseed">
Rand
</a-button>
<a-button @click="resetTestseed">
Reset
</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider :style="{ margin: '5px 0' }"></a-divider>
</template>
{{end}}

View File

@@ -0,0 +1,70 @@
{{define "form/streamFinalMask"}}
<a-divider :style="{ margin: '5px 0 0' }"></a-divider>
<a-form :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-form-item label="UDP Masks">
<a-button icon="plus" type="primary" size="small"
@click="inbound.stream.addUdpMask(inbound.stream.network === 'kcp' ? 'mkcp-aes128gcm' : 'xdns')"></a-button>
</a-form-item>
<template
v-if="inbound.stream.finalmask.udp && inbound.stream.finalmask.udp.length > 0">
<a-form v-for="(mask, index) in inbound.stream.finalmask.udp"
: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="() => inbound.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"
@change="(type) => mask.settings = mask._getDefaultSettings(type, {})"
:dropdown-class-name="themeSwitcher.currentTheme">
<!-- mKCP-specific masks -->
<a-select-option v-if="inbound.stream.network === 'kcp'"
value="mkcp-aes128gcm">
mKCP AES-128-GCM</a-select-option>
<a-select-option v-if="inbound.stream.network === 'kcp'"
value="header-dns">
Header DNS</a-select-option>
<a-select-option v-if="inbound.stream.network === 'kcp'"
value="header-dtls">
Header DTLS 1.2</a-select-option>
<a-select-option v-if="inbound.stream.network === 'kcp'"
value="header-srtp">
Header SRTP</a-select-option>
<a-select-option v-if="inbound.stream.network === 'kcp'"
value="header-utp">
Header uTP</a-select-option>
<a-select-option v-if="inbound.stream.network === 'kcp'"
value="header-wechat">
Header WeChat Video</a-select-option>
<a-select-option v-if="inbound.stream.network === 'kcp'"
value="header-wireguard">
Header WireGuard</a-select-option>
<a-select-option v-if="inbound.stream.network === 'kcp'"
value="mkcp-original">
mKCP Original</a-select-option>
<!-- xDNS for TCP/WS/HTTPUpgrade/XHTTP -->
<a-select-option
v-if="['tcp', 'ws', 'httpupgrade', 'xhttp'].includes(inbound.stream.network)"
value="xdns">
xDNS (Experimental)</a-select-option>
</a-select>
</a-form-item>
<!-- Settings for password-based masks -->
<a-form-item label='Password'
v-if="['mkcp-aes128gcm'].includes(mask.type)">
<a-input v-model.trim="mask.settings.password"
placeholder="Obfuscation password"></a-input>
</a-form-item>
<!-- Settings for domain-based masks -->
<a-form-item label='Domain'
v-if="['header-dns', 'xdns'].includes(mask.type)">
<a-input v-model.trim="mask.settings.domain"
placeholder="e.g., www.example.com"></a-input>
</a-form-item>
</a-form>
</template>
</a-form>
{{end}}

View File

@@ -1,48 +1,32 @@
{{define "form/streamKCP"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="inbound.stream.kcp.type" :style="{ width: '50%' }" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">None</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="wechat-video">WeChat</a-select-option>
<a-select-option value="dtls">DTLS 1.2</a-select-option>
<a-select-option value="wireguard">WireGuard</a-select-option>
<a-select-option value="dns">DNS</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template>
{{ i18n "password" }}
<a-icon @click="inbound.stream.kcp.seed = RandomUtil.randomSeq(10)"type="sync"> </a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="inbound.stream.kcp.seed"></a-input>
</a-form-item>
<a-form :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-form-item label='MTU'>
<a-input-number v-model.number="inbound.stream.kcp.mtu" :min="576" :max="1460"></a-input-number>
<a-input-number v-model.number="inbound.stream.kcp.mtu" :min="576"
:max="1460"></a-input-number>
</a-form-item>
<a-form-item label='TTI (ms)'>
<a-input-number v-model.number="inbound.stream.kcp.tti" :min="10" :max="100"></a-input-number>
<a-input-number v-model.number="inbound.stream.kcp.tti" :min="10"
:max="100"></a-input-number>
</a-form-item>
<a-form-item label='Uplink (MB/s)'>
<a-input-number v-model.number="inbound.stream.kcp.upCap" :min="0"></a-input-number>
</a-form-item>
<a-input-number v-model.number="inbound.stream.kcp.upCap"
:min="0"></a-input-number>
</a-form-item>
<a-form-item label='Downlink (MB/s)'>
<a-input-number v-model.number="inbound.stream.kcp.downCap" :min="0"></a-input-number>
<a-input-number v-model.number="inbound.stream.kcp.downCap"
:min="0"></a-input-number>
</a-form-item>
<a-form-item label='Congestion'>
<a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
</a-form-item>
<a-form-item label='Read Buffer (MB)'>
<a-input-number v-model.number="inbound.stream.kcp.readBuffer" :min="0"></a-input-number>
<a-input-number v-model.number="inbound.stream.kcp.readBuffer"
:min="0"></a-input-number>
</a-form-item>
<a-form-item label='Write Buffer (MB)'>
<a-input-number v-model.number="inbound.stream.kcp.writeBuffer" :min="0"></a-input-number>
<a-input-number v-model.number="inbound.stream.kcp.writeBuffer"
:min="0"></a-input-number>
</a-form-item>
</a-form>
{{end}}

View File

@@ -1,8 +1,10 @@
{{define "form/streamSettings"}}
<!-- select stream network -->
<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 "transmission" }}'>
<a-select v-model="inbound.stream.network" :style="{ width: '75%' }" @change="streamNetworkChange"
<a-select v-model="inbound.stream.network" :style="{ width: '75%' }"
@change="streamNetworkChange"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tcp">TCP (RAW)</a-select-option>
<a-select-option value="kcp">mKCP</a-select-option>
@@ -48,4 +50,10 @@
<template>
{{template "form/streamSockopt"}}
</template>
<!-- finalmask - only for TCP, WS, HTTPUpgrade, XHTTP, mKCP -->
<template
v-if="['tcp', 'ws', 'httpupgrade', 'xhttp', 'kcp'].includes(inbound.stream.network)">
{{template "form/streamFinalMask"}}
</template>
{{end}}

View File

@@ -1,5 +1,6 @@
{{define "form/streamXHTTP"}}
<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 "host" }}'>
<a-input v-model.trim="inbound.stream.xhttp.host"></a-input>
</a-form-item>
@@ -7,38 +8,138 @@
<a-input v-model.trim="inbound.stream.xhttp.path"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button icon="plus" size="small" @click="inbound.stream.xhttp.addHeader('', '')"></a-button>
<a-button icon="plus" size="small"
@click="inbound.stream.xhttp.addHeader('', '')"></a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.xhttp.headers">
<a-input-group compact
v-for="(header, index) in inbound.stream.xhttp.headers">
<a-input :style="{ width: '50%' }" v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
<template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
<template slot="addonBefore" :style="{ margin: '0' }">[[ index+1
]]</template>
</a-input>
<a-input :style="{ width: '50%' }" v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.xhttp.removeHeader(index)"></a-button>
<a-button icon="minus" slot="addonAfter" size="small"
@click="inbound.stream.xhttp.removeHeader(index)"></a-button>
</a-input>
</a-input-group>
</a-form-item>
<a-form-item label='Mode'>
<a-select v-model="inbound.stream.xhttp.mode" :style="{ width: '50%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in MODE_OPTION" :value="key">[[ key ]]</a-select-option>
<a-select-option v-for="key in MODE_OPTION" :value="key">[[ key
]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Max Buffered Upload" v-if="inbound.stream.xhttp.mode === 'packet-up'">
<a-input-number v-model.number="inbound.stream.xhttp.scMaxBufferedPosts"></a-input-number>
<a-form-item label="Max Buffered Upload"
v-if="inbound.stream.xhttp.mode === 'packet-up'">
<a-input-number
v-model.number="inbound.stream.xhttp.scMaxBufferedPosts"></a-input-number>
</a-form-item>
<a-form-item label="Max Upload Size (Byte)" v-if="inbound.stream.xhttp.mode === 'packet-up'">
<a-input v-model.trim="inbound.stream.xhttp.scMaxEachPostBytes"></a-input>
<a-form-item label="Max Upload Size (Byte)"
v-if="inbound.stream.xhttp.mode === 'packet-up'">
<a-input
v-model.trim="inbound.stream.xhttp.scMaxEachPostBytes"></a-input>
</a-form-item>
<a-form-item label="Stream-Up Server" v-if="inbound.stream.xhttp.mode === 'stream-up'">
<a-input v-model.trim="inbound.stream.xhttp.scStreamUpServerSecs"></a-input>
<a-form-item label="Stream-Up Server"
v-if="inbound.stream.xhttp.mode === 'stream-up'">
<a-input
v-model.trim="inbound.stream.xhttp.scStreamUpServerSecs"></a-input>
</a-form-item>
<a-form-item label="Padding Bytes">
<a-input v-model.trim="inbound.stream.xhttp.xPaddingBytes"></a-input>
</a-form-item>
<a-form-item label="Padding Obfs Mode">
<a-switch v-model="inbound.stream.xhttp.xPaddingObfsMode"></a-switch>
</a-form-item>
<template v-if="inbound.stream.xhttp.xPaddingObfsMode">
<a-form-item label="Padding Key">
<a-input v-model.trim="inbound.stream.xhttp.xPaddingKey"
placeholder="x_padding"></a-input>
</a-form-item>
<a-form-item label="Padding Header">
<a-input v-model.trim="inbound.stream.xhttp.xPaddingHeader"
placeholder="X-Padding"></a-input>
</a-form-item>
<a-form-item label="Padding Placement">
<a-select v-model="inbound.stream.xhttp.xPaddingPlacement"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value>Default (queryInHeader)</a-select-option>
<a-select-option
value="queryInHeader">queryInHeader</a-select-option>
<a-select-option value="header">header</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Padding Method">
<a-select v-model="inbound.stream.xhttp.xPaddingMethod"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value>Default (repeat-x)</a-select-option>
<a-select-option value="repeat-x">repeat-x</a-select-option>
<a-select-option value="tokenish">tokenish</a-select-option>
</a-select>
</a-form-item>
</template>
<a-form-item label="Uplink HTTP Method">
<a-select v-model="inbound.stream.xhttp.uplinkHTTPMethod"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value>Default (POST)</a-select-option>
<a-select-option value="POST">POST</a-select-option>
<a-select-option value="PUT">PUT</a-select-option>
<a-select-option value="GET">GET (packet-up only)</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Session Placement">
<a-select v-model="inbound.stream.xhttp.sessionPlacement"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value>Default (path)</a-select-option>
<a-select-option value="path">path</a-select-option>
<a-select-option value="header">header</a-select-option>
<a-select-option value="cookie">cookie</a-select-option>
<a-select-option value="query">query</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Session Key"
v-if="inbound.stream.xhttp.sessionPlacement && inbound.stream.xhttp.sessionPlacement !== 'path'">
<a-input v-model.trim="inbound.stream.xhttp.sessionKey"
placeholder="x_session"></a-input>
</a-form-item>
<a-form-item label="Sequence Placement">
<a-select v-model="inbound.stream.xhttp.seqPlacement"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value>Default (path)</a-select-option>
<a-select-option value="path">path</a-select-option>
<a-select-option value="header">header</a-select-option>
<a-select-option value="cookie">cookie</a-select-option>
<a-select-option value="query">query</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Sequence Key"
v-if="inbound.stream.xhttp.seqPlacement && inbound.stream.xhttp.seqPlacement !== 'path'">
<a-input v-model.trim="inbound.stream.xhttp.seqKey"
placeholder="x_seq"></a-input>
</a-form-item>
<a-form-item label="Uplink Data Placement"
v-if="inbound.stream.xhttp.mode === 'packet-up'">
<a-select v-model="inbound.stream.xhttp.uplinkDataPlacement"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value>Default (body)</a-select-option>
<a-select-option value="body">body</a-select-option>
<a-select-option value="header">header</a-select-option>
<a-select-option value="query">query</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Uplink Data Key"
v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'">
<a-input v-model.trim="inbound.stream.xhttp.uplinkDataKey"
placeholder="x_data"></a-input>
</a-form-item>
<a-form-item label="Uplink Chunk Size"
v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'">
<a-input-number v-model.number="inbound.stream.xhttp.uplinkChunkSize"
:min="0" placeholder="0 (unlimited)"></a-input-number>
</a-form-item>
<a-form-item label="No SSE Header">
<a-switch v-model="inbound.stream.xhttp.noSSEHeader"></a-switch>
</a-form-item>

View File

@@ -1,11 +1,13 @@
{{define "form/tlsSettings"}}
<!-- tls enable -->
<a-form v-if="inbound.canEnableTls()" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form v-if="inbound.canEnableTls()" :colon="false"
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-divider :style="{ margin: '3px 0' }"></a-divider>
<a-form-item label='{{ i18n "security" }}'>
<a-radio-group v-model="inbound.stream.security" button-style="solid">
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
<a-radio-button v-if="inbound.canEnableReality()" value="reality">Reality</a-radio-button>
<a-radio-button v-if="inbound.canEnableReality()"
value="reality">Reality</a-radio-button>
<a-radio-button value="tls">TLS</a-radio-button>
</a-radio-group>
</a-form-item>
@@ -16,33 +18,44 @@
<a-input v-model.trim="inbound.stream.tls.sni"></a-input>
</a-form-item>
<a-form-item label="Cipher Suites">
<a-select v-model="inbound.stream.tls.cipherSuites" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="">Auto</a-select-option>
<a-select-option v-for="key,value in TLS_CIPHER_OPTION" :value="key">[[ value ]]</a-select-option>
<a-select v-model="inbound.stream.tls.cipherSuites"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value>Auto</a-select-option>
<a-select-option v-for="key,value in TLS_CIPHER_OPTION" :value="key">[[
value ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Min/Max Version">
<a-input-group compact>
<a-select v-model="inbound.stream.tls.minVersion" :style="{ width: '50%' }"
<a-select v-model="inbound.stream.tls.minVersion"
:style="{ width: '50%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key
]]</a-select-option>
</a-select>
<a-select v-model="inbound.stream.tls.maxVersion" :style="{ width: '50%' }"
<a-select v-model="inbound.stream.tls.maxVersion"
:style="{ width: '50%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key
]]</a-select-option>
</a-select>
</a-input-group>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="inbound.stream.tls.settings.fingerprint" :style="{ width: '100%' }"
<a-select v-model="inbound.stream.tls.settings.fingerprint"
:style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<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-option value>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key
]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="ALPN">
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" v-model="inbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
<a-select mode="multiple"
:dropdown-class-name="themeSwitcher.currentTheme"
v-model="inbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn
]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Allow Insecure">
@@ -57,21 +70,25 @@
<a-form-item label="Session Resumption">
<a-switch v-model="inbound.stream.tls.enableSessionResumption"></a-switch>
</a-form-item>
<a-form-item label="VerifyPeerCertInNames">
<a-input v-model.trim="inbound.stream.tls.verifyPeerCertInNames"></a-input>
</a-form-item>
<a-divider :style="{ margin: '3px 0' }"></a-divider>
<template v-for="cert,index in inbound.stream.tls.certs">
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid" :style="{ display: 'inline-flex', whiteSpace: 'nowrap', maxWidth: '100%' }">
<a-radio-button :value="true" :style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false" :style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
<a-radio-group v-model="cert.useFile" button-style="solid"
:style="{ display: 'inline-flex', whiteSpace: 'nowrap', maxWidth: '100%' }">
<a-radio-button :value="true"
:style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{
i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false"
:style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{
i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label=" ">
<a-space>
<a-button icon="plus" v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()"></a-button>
<a-button icon="minus" v-if="inbound.stream.tls.certs.length>1" type="primary" size="small"
<a-button icon="plus" v-if="index === 0" type="primary" size="small"
@click="inbound.stream.tls.addCert()"></a-button>
<a-button icon="minus" v-if="inbound.stream.tls.certs.length>1"
type="primary" size="small"
@click="inbound.stream.tls.removeCert(index)"></a-button>
</a-space>
</a-form-item>
@@ -83,7 +100,8 @@
<a-input v-model.trim="cert.keyFile"></a-input>
</a-form-item>
<a-form-item label=" ">
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">
<a-button type="primary" icon="import"
@click="setDefaultCertData(index)">
{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</a-form-item>
</template>
@@ -99,8 +117,10 @@
<a-switch v-model="cert.oneTimeLoading"></a-switch>
</a-form-item>
<a-form-item label='Usage Option'>
<a-select v-model="cert.usage" :style="{ width: '50%' }" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in USAGE_OPTION" :value="key">[[ key ]]</a-select-option>
<a-select v-model="cert.usage" :style="{ width: '50%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in USAGE_OPTION" :value="key">[[ key
]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Build Chain" v-if="cert.usage === 'issue'">
@@ -108,20 +128,22 @@
</a-form-item>
</template>
<a-form-item label='ECH key'>
<a-input v-model="inbound.stream.tls.echServerKeys"></a-input>
<a-input v-model="inbound.stream.tls.echServerKeys"></a-input>
</a-form-item>
<a-form-item label='ECH config'>
<a-input v-model="inbound.stream.tls.settings.echConfigList"></a-input>
<a-input v-model="inbound.stream.tls.settings.echConfigList"></a-input>
</a-form-item>
<a-form-item label='ECH force query'>
<a-select v-model="inbound.stream.tls.echForceQuery"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in ['none', 'half', 'full']" :value="key">[[ key ]]</a-select-option>
</a-select>
<a-select v-model="inbound.stream.tls.echForceQuery"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in ['none', 'half', 'full']" :value="key">[[
key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label=" ">
<a-space>
<a-button type="primary" icon="import" @click="getNewEchCert">Get New ECH Cert</a-button>
<a-button type="primary" icon="import" @click="getNewEchCert">Get New
ECH Cert</a-button>
<a-button danger @click="clearEchCert">Clear</a-button>
</a-space>
</a-form-item>

View File

@@ -1602,41 +1602,42 @@
if (payload && Array.isArray(payload)) {
// Use setInbounds to properly convert to DBInbound objects with methods
this.setInbounds(payload);
this.searchInbounds(this.searchKey);
}
});
// Listen for traffic updates
window.wsClient.on('traffic', (payload) => {
if (payload && payload.clientTraffics && Array.isArray(payload.clientTraffics)) {
// Update client traffic statistics
payload.clientTraffics.forEach(clientTraffic => {
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;
}
}
});
}
// Note: Do NOT update total consumed traffic (stats.up, stats.down) from this event
// because clientTraffics contains delta/incremental values, not total accumulated values.
// Total traffic is updated via the 'inbounds' event which contains accumulated values from database.
// Update online clients list in real-time
if (payload && Array.isArray(payload.onlineClients)) {
this.onlineClients = payload.onlineClients;
// 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);
const nextOnlineClients = payload.onlineClients;
let onlineChanged = this.onlineClients.length !== nextOnlineClients.length;
if (!onlineChanged) {
const prevSet = new Set(this.onlineClients);
for (const email of nextOnlineClients) {
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
@@ -1645,8 +1646,6 @@
}
});
// Notifications disabled - white notifications are not needed
// Fallback to polling if WebSocket fails
window.wsClient.on('error', () => {
console.warn('WebSocket connection failed, falling back to polling');

View File

@@ -15,13 +15,6 @@
<a-switch v-model="allSetting.subJsonEnable"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subTitle"}}</template>
<template #description>{{ i18n "pages.settings.subTitleDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.subTitle"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subListen"}}</template>
<template #description>{{ i18n "pages.settings.subListenDesc"}}</template>
@@ -78,6 +71,50 @@
<a-switch v-model="allSetting.subShowInfo"></a-switch>
</template>
</a-setting-list-item>
<a-divider>{{ i18n "pages.xray.basicTemplate"}}</a-divider>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subTitle"}}</template>
<template #description>{{ i18n "pages.settings.subTitleDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.subTitle"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subSupportUrl"}}</template>
<template #description>{{ i18n "pages.settings.subSupportUrlDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.subSupportUrl" placeholder="https://example.com"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subProfileUrl"}}</template>
<template #description>{{ i18n "pages.settings.subProfileUrlDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.subProfileUrl" placeholder="https://example.com"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subAnnounce"}}</template>
<template #description>{{ i18n "pages.settings.subAnnounceDesc"}}</template>
<template #control>
<a-textarea v-model="allSetting.subAnnounce"></a-textarea>
</template>
</a-setting-list-item>
<a-divider>{{ i18n "pages.xray.advancedTemplate"}} (Happ)</a-divider>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subEnableRouting"}}</template>
<template #description>{{ i18n "pages.settings.subEnableRoutingDesc"}}</template>
<template #control>
<a-switch v-model="allSetting.subEnableRouting"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subRoutingRules"}}</template>
<template #description>{{ i18n "pages.settings.subRoutingRulesDesc"}}</template>
<template #control>
<a-textarea v-model="allSetting.subRoutingRules" placeholder="happ://routing/add/..."></a-textarea>
</template>
</a-setting-list-item>
</a-collapse-panel>
<a-collapse-panel key="3" header='{{ i18n "pages.settings.certs" }}'>
<a-setting-list-item paddings="small">

View File

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

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ func (j *ClearLogsJob) Run() {
}
// Clear log files and copy to previous logs
for i := 0; i < len(logFiles); i++ {
for i := range len(logFiles) {
if i > 0 {
// Copy to previous logs
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)
}
// 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{}{
"traffics": traffics,
"clientTraffics": clientTraffics,
@@ -66,6 +78,16 @@ func (j *XrayTrafficJob) Run() {
"lastOnlineMap": lastOnlineMap,
}
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) {

View File

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

View File

@@ -567,7 +567,7 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
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 >= 31) {
versions = append(versions, release.TagName)
}
}
@@ -1205,7 +1205,7 @@ func (s *ServerService) GetNewmldsa65() (any, error) {
return keyPair, nil
}
func (s *ServerService) GetNewEchCert(sni string) (interface{}, error) {
func (s *ServerService) GetNewEchCert(sni string) (any, error) {
// Run the command
cmd := exec.Command(xray.GetBinaryPath(), "tls", "ech", "--serverName", sni)
var out bytes.Buffer
@@ -1223,7 +1223,7 @@ func (s *ServerService) GetNewEchCert(sni string) (interface{}, error) {
configList := lines[1]
serverKeys := lines[3]
return map[string]interface{}{
return map[string]any{
"echServerKeys": serverKeys,
"echConfigList": configList,
}, nil

View File

@@ -53,6 +53,11 @@ var defaultValueMap = map[string]string{
"subEnable": "true",
"subJsonEnable": "false",
"subTitle": "",
"subSupportUrl": "",
"subProfileUrl": "",
"subAnnounce": "",
"subEnableRouting": "true",
"subRoutingRules": "",
"subListen": "",
"subPort": "2096",
"subPath": "/sub/",
@@ -459,6 +464,26 @@ func (s *SettingService) GetSubTitle() (string, error) {
return s.getString("subTitle")
}
func (s *SettingService) GetSubSupportUrl() (string, error) {
return s.getString("subSupportUrl")
}
func (s *SettingService) GetSubProfileUrl() (string, error) {
return s.getString("subProfileUrl")
}
func (s *SettingService) GetSubAnnounce() (string, error) {
return s.getString("subAnnounce")
}
func (s *SettingService) GetSubEnableRouting() (bool, error) {
return s.getBool("subEnableRouting")
}
func (s *SettingService) GetSubRoutingRules() (string, error) {
return s.getString("subRoutingRules")
}
func (s *SettingService) GetSubListen() (string, error) {
return s.getString("subListen")
}

View File

@@ -2267,6 +2267,8 @@ func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {
}
// Gather settings to construct absolute URLs
subURI, _ := t.settingService.GetSubURI()
subJsonURI, _ := t.settingService.GetSubJsonURI()
subDomain, _ := t.settingService.GetSubDomain()
subPort, _ := t.settingService.GetSubPort()
subPath, _ := t.settingService.GetSubPath()
@@ -2314,8 +2316,29 @@ func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {
subJsonPath = subJsonPath + "/"
}
subURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subPath, client.SubID)
subJsonURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subJsonPath, client.SubID)
var subURL string
var subJsonURL string
// If pre-configured URIs are available, use them directly
if subURI != "" {
if !strings.HasSuffix(subURI, "/") {
subURI = subURI + "/"
}
subURL = fmt.Sprintf("%s%s", subURI, client.SubID)
} else {
subURL = fmt.Sprintf("%s://%s%s%s", scheme, host, subPath, client.SubID)
}
if subJsonURI != "" {
if !strings.HasSuffix(subJsonURI, "/") {
subJsonURI = subJsonURI + "/"
}
subJsonURL = fmt.Sprintf("%s%s", subJsonURI, client.SubID)
} else {
subJsonURL = fmt.Sprintf("%s://%s%s%s", scheme, host, subJsonPath, client.SubID)
}
if !subJsonEnable {
subJsonURL = ""
}

View File

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

View File

@@ -374,6 +374,16 @@
"subJsonEnable" = "تمكين/تعطيل نقطة نهاية اشتراك JSON بشكل مستقل."
"subTitle" = "عنوان الاشتراك"
"subTitleDesc" = "العنوان اللي هيظهر في عميل VPN"
"subSupportUrl" = "رابط الدعم"
"subSupportUrlDesc" = "رابط الدعم الفني المعروض في عميل VPN"
"subProfileUrl" = "رابط الملف الشخصي"
"subProfileUrlDesc" = "رابط لموقعك الإلكتروني يظهر في عميل VPN"
"subAnnounce" = "إعلان"
"subAnnounceDesc" = "نص الإعلان المعروض في عميل VPN"
"subEnableRouting" = "تفعيل التوجيه"
"subEnableRoutingDesc" = "إعداد عام لتمكين التوجيه (Routing) في عميل VPN. (فقط لـ Happ)"
"subRoutingRules" = "قواعد التوجيه"
"subRoutingRulesDesc" = "قواعد التوجيه العامة لعميل VPN. (فقط لـ Happ)"
"subListen" = "IP الاستماع"
"subListenDesc" = "عنوان IP لخدمة الاشتراك. (سيبه فاضي عشان يستمع على كل الـ IPs)"
"subPort" = "بورت الاستماع"
@@ -531,6 +541,12 @@
"psk" = "المفتاح المشترك"
"domainStrategy" = "استراتيجية الدومين"
[pages.xray.tun]
"nameDesc" = "اسم واجهة TUN. القيمة الافتراضية هي 'xray0'"
"mtuDesc" = "وحدة النقل الأقصى. الحد الأقصى لحجم حزم البيانات. القيمة الافتراضية هي 1500"
"userLevel" = "مستوى المستخدم"
"userLevelDesc" = "ستستخدم جميع الاتصالات المُرسلة عبر هذا الإدخال مستوى المستخدم هذا. القيمة الافتراضية هي 0"
[pages.xray.dns]
"enable" = "فعل DNS"
"enableDesc" = "فعل سيرفر DNS المدمج"

View File

@@ -374,6 +374,16 @@
"subJsonEnable" = "Enable/Disable the JSON subscription endpoint independently."
"subTitle" = "Subscription Title"
"subTitleDesc" = "Title shown in VPN client"
"subSupportUrl" = "Support URL"
"subSupportUrlDesc" = "Technical support link shown in the VPN client"
"subProfileUrl" = "Profile URL"
"subProfileUrlDesc" = "A link to your website displayed in the VPN client"
"subAnnounce" = "Announce"
"subAnnounceDesc" = "The text of the announce displayed in the VPN client"
"subEnableRouting" = "Enable routing"
"subEnableRoutingDesc" = "Global setting to enable routing in the VPN client. (Only for Happ)"
"subRoutingRules" = "Routing rules"
"subRoutingRulesDesc" = "Global routing rules for the VPN client. (Only for Happ)"
"subListen" = "Listen IP"
"subListenDesc" = "The IP address for the subscription service. (leave blank to listen on all IPs)"
"subPort" = "Listen Port"
@@ -531,6 +541,12 @@
"psk" = "PreShared Key"
"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]
"enable" = "Enable DNS"
"enableDesc" = "Enable built-in DNS server"

View File

@@ -374,6 +374,16 @@
"subJsonEnable" = "Habilitar/Deshabilitar el endpoint de suscripción JSON de forma independiente."
"subTitle" = "Título de la Suscripción"
"subTitleDesc" = "Título mostrado en el cliente de VPN"
"subSupportUrl" = "URL de soporte"
"subSupportUrlDesc" = "Enlace de soporte técnico mostrado en el cliente VPN"
"subProfileUrl" = "URL del perfil"
"subProfileUrlDesc" = "Un enlace a tu sitio web mostrado en el cliente VPN"
"subAnnounce" = "Anuncio"
"subAnnounceDesc" = "El texto del anuncio mostrado en el cliente VPN"
"subEnableRouting" = "Habilitar enrutamiento"
"subEnableRoutingDesc" = "Configuración global para habilitar el enrutamiento en el cliente VPN. (Solo para Happ)"
"subRoutingRules" = "Reglas de enrutamiento"
"subRoutingRulesDesc" = "Reglas de enrutamiento globales para el cliente VPN. (Solo para Happ)"
"subListen" = "Listening IP"
"subListenDesc" = "Dejar en blanco por defecto para monitorear todas las IPs."
"subPort" = "Puerto de Suscripción"
@@ -531,6 +541,12 @@
"psk" = "Clave precompartida"
"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]
"enable" = "Habilitar DNS"
"enableDesc" = "Habilitar servidor DNS incorporado"

View File

@@ -374,6 +374,16 @@
"subJsonEnable" = "فعال/غیرفعال‌سازی مستقل نقطه دسترسی سابسکریپشن JSON."
"subTitle" = "عنوان اشتراک"
"subTitleDesc" = "عنوان نمایش داده شده در کلاینت VPN"
"subSupportUrl" = "آدرس پشتیبانی"
"subSupportUrlDesc" = "لینک پشتیبانی فنی که در کلاینت VPN نمایش داده می‌شود"
"subProfileUrl" = "آدرس پروفایل"
"subProfileUrlDesc" = "لینک وب‌سایت شما که در کلاینت VPN نمایش داده می‌شود"
"subAnnounce" = "اعلان"
"subAnnounceDesc" = "متن اعلانی که در کلاینت VPN نمایش داده می‌شود"
"subEnableRouting" = "فعال‌سازی مسیریابی"
"subEnableRoutingDesc" = "تنظیمات سراسری برای فعال‌سازی مسیریابی در کلاینت VPN. (فقط برای Happ)"
"subRoutingRules" = "قوانین مسیریابی"
"subRoutingRulesDesc" = "قوانین مسیریابی سراسری برای کلاینت VPN. (فقط برای Happ)"
"subListen" = "آدرس آی‌پی"
"subListenDesc" = "آدرس آی‌پی برای سرویس سابسکریپشن. برای گوش دادن به‌تمام آی‌پی‌ها خالی‌بگذارید"
"subPort" = "پورت"
@@ -531,6 +541,12 @@
"psk" = "کلید مشترک"
"domainStrategy" = "استراتژی حل دامنه"
[pages.xray.tun]
"nameDesc" = "نام رابط TUN. مقدار پیش‌فرض 'xray0' است"
"mtuDesc" = "واحد انتقال حداکثر. بیشترین اندازه بسته‌های داده. مقدار پیش‌فرض 1500 است"
"userLevel" = "سطح کاربر"
"userLevelDesc" = "تمام اتصالات انجام‌شده از طریق این ورودی از این سطح کاربری استفاده خواهند کرد. مقدار پیش‌فرض 0 است"
[pages.xray.dns]
"enable" = "فعال کردن حل دامنه"
"enableDesc" = "سرور حل دامنه داخلی را فعال کنید"

View File

@@ -374,6 +374,16 @@
"subJsonEnable" = "Aktifkan/Nonaktifkan endpoint langganan JSON secara mandiri."
"subTitle" = "Judul Langganan"
"subTitleDesc" = "Judul yang ditampilkan di klien VPN"
"subSupportUrl" = "URL Dukungan"
"subSupportUrlDesc" = "Tautan dukungan teknis yang ditampilkan di klien VPN"
"subProfileUrl" = "URL Profil"
"subProfileUrlDesc" = "Tautan ke situs web Anda yang ditampilkan di klien VPN"
"subAnnounce" = "Pengumuman"
"subAnnounceDesc" = "Teks pengumuman yang ditampilkan di klien VPN"
"subEnableRouting" = "Aktifkan perutean"
"subEnableRoutingDesc" = "Pengaturan global untuk mengaktifkan perutean (routing) di klien VPN. (Hanya untuk Happ)"
"subRoutingRules" = "Aturan routing"
"subRoutingRulesDesc" = "Aturan routing global untuk klien VPN. (Hanya untuk Happ)"
"subListen" = "IP Pendengar"
"subListenDesc" = "Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)"
"subPort" = "Port Pendengar"
@@ -531,6 +541,12 @@
"psk" = "Kunci Pra-Bagi"
"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]
"enable" = "Aktifkan DNS"
"enableDesc" = "Aktifkan server DNS bawaan"

View File

@@ -374,6 +374,16 @@
"subJsonEnable" = "JSON サブスクリプションのエンドポイントを個別に有効/無効にする。"
"subTitle" = "サブスクリプションタイトル"
"subTitleDesc" = "VPNクライアントに表示されるタイトル"
"subSupportUrl" = "サポートURL"
"subSupportUrlDesc" = "VPNクライアントに表示されるテクニカルサポートへのリンク"
"subProfileUrl" = "プロフィールURL"
"subProfileUrlDesc" = "VPNクライアントに表示されるWebサイトへのリンク"
"subAnnounce" = "お知らせ"
"subAnnounceDesc" = "VPNクライアントに表示されるお知らせのテキスト"
"subEnableRouting" = "ルーティングを有効化"
"subEnableRoutingDesc" = "VPNクライアントでルーティングを有効にするためのグローバル設定。(Happのみ)"
"subRoutingRules" = "ルーティングルール"
"subRoutingRulesDesc" = "VPNクライアントのグローバルルーティングルール。(Happのみ)"
"subListen" = "監視IP"
"subListenDesc" = "サブスクリプションサービスが監視するIPアドレス空白にするとすべてのIPを監視"
"subPort" = "監視ポート"
@@ -531,6 +541,12 @@
"psk" = "共有キー"
"domainStrategy" = "ドメイン戦略"
[pages.xray.tun]
"nameDesc" = "TUN インターフェースの名前。デフォルトは 'xray0' です"
"mtuDesc" = "最大伝送単位。データパケットの最大サイズ。デフォルトは 1500 です"
"userLevel" = "ユーザーレベル"
"userLevelDesc" = "このインバウンドを通じて確立されたすべての接続は、このユーザーレベルを使用します。デフォルトは 0 です"
[pages.xray.dns]
"enable" = "DNSを有効にする"
"enableDesc" = "組み込みDNSサーバーを有効にする"

View File

@@ -374,6 +374,16 @@
"subJsonEnable" = "Ativar/Desativar o endpoint de assinatura JSON de forma independente."
"subTitle" = "Título da Assinatura"
"subTitleDesc" = "Título exibido no cliente VPN"
"subSupportUrl" = "URL de Suporte"
"subSupportUrlDesc" = "Link de suporte técnico exibido no cliente VPN"
"subProfileUrl" = "URL de Perfil"
"subProfileUrlDesc" = "Um link para o seu site exibido no cliente VPN"
"subAnnounce" = "Anúncio"
"subAnnounceDesc" = "O texto do anúncio exibido no cliente VPN"
"subEnableRouting" = "Ativar roteamento"
"subEnableRoutingDesc" = "Configuração global para habilitar o roteamento no cliente VPN. (Apenas para Happ)"
"subRoutingRules" = "Regras de roteamento"
"subRoutingRulesDesc" = "Regras de roteamento globais para o cliente VPN. (Apenas para Happ)"
"subListen" = "IP de Escuta"
"subListenDesc" = "O endereço IP para o serviço de assinatura. (deixe em branco para escutar em todos os IPs)"
"subPort" = "Porta de Escuta"
@@ -531,6 +541,12 @@
"psk" = "Chave Pré-Compartilhada"
"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]
"enable" = "Ativar DNS"
"enableDesc" = "Ativar o servidor DNS integrado"

View File

@@ -373,7 +373,17 @@
"subEnableDesc" = "Функция подписки с отдельной конфигурацией"
"subJsonEnable" = "Включить/отключить JSON-эндпоинт подписки независимо."
"subTitle" = "Заголовок подписки"
"subTitleDesc" = "Название подписки, которое видит клиент в VPN клиенте"
"subTitleDesc" = "Название подписки, которое видит клиент в VPN-клиенте"
"subSupportUrl" = "URL поддержки"
"subSupportUrlDesc" = "Ссылка на техническую поддержку, отображаемая в VPN-клиенте"
"subProfileUrl" = "URL профиля"
"subProfileUrlDesc" = "Ссылка на ваш сайт, отображаемая в VPN-клиенте"
"subAnnounce" = "Объявление"
"subAnnounceDesc" = "Текст объявления, отображаемый в VPN-клиенте"
"subEnableRouting" = "Включить маршрутизацию"
"subEnableRoutingDesc" = "Глобальная настройка для включения маршрутизации в VPN-клиенте. (Только для Happ)"
"subRoutingRules" = "Правила маршрутизации"
"subRoutingRulesDesc" = "Глобальные правила маршрутизации для VPN-клиента. (Только для Happ)"
"subListen" = "Прослушивание IP"
"subListenDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все IP-адреса"
"subPort" = "Порт подписки"
@@ -531,6 +541,12 @@
"psk" = "Общий ключ"
"domainStrategy" = "Стратегия домена"
[pages.xray.tun]
"nameDesc" = "Имя интерфейса TUN. Значение по умолчанию - 'xray0'"
"mtuDesc" = "Максимальная единица передачи. Максимальный размер пакетов данных. Значение по умолчанию - 1500"
"userLevel" = "Уровень пользователя"
"userLevelDesc" = "Все соединения, установленные через этот входящий поток, будут использовать этот уровень пользователя. Значение по умолчанию - 0"
[pages.xray.dns]
"enable" = "Включить DNS"
"enableDesc" = "Включить встроенный DNS-сервер"

View File

@@ -374,6 +374,16 @@
"subJsonEnable" = "JSON abonelik uç noktasını bağımsız olarak Etkinleştir/Devre Dışı bırak."
"subTitle" = "Abonelik Başlığı"
"subTitleDesc" = "VPN istemcisinde gösterilen başlık"
"subSupportUrl" = "Destek URL'si"
"subSupportUrlDesc" = "VPN istemcisinde gösterilen teknik destek bağlantısı"
"subProfileUrl" = "Profil URL'si"
"subProfileUrlDesc" = "VPN istemcisinde görüntülenen web sitenize giden bağlantı"
"subAnnounce" = "Duyuru"
"subAnnounceDesc" = "VPN istemcisinde görüntülenen duyuru metni"
"subEnableRouting" = "Yönlendirmeyi etkinleştir"
"subEnableRoutingDesc" = "VPN istemcisinde yönlendirmeyi etkinleştirmek için genel ayar. (Yalnızca Happ için)"
"subRoutingRules" = "Yönlendirme kuralları"
"subRoutingRulesDesc" = "VPN istemcisi için genel yönlendirme kuralları. (Yalnızca Happ için)"
"subListen" = "Dinleme IP"
"subListenDesc" = "Abonelik hizmeti için IP adresi. (tüm IP'leri dinlemek için boş bırakın)"
"subPort" = "Dinleme Portu"
@@ -531,6 +541,12 @@
"psk" = "Ön Paylaşılan Anahtar"
"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]
"enable" = "DNS'yi Etkinleştir"
"enableDesc" = "Dahili DNS sunucusunu etkinleştir"

View File

@@ -374,6 +374,16 @@
"subJsonEnable" = "Увімкнути/вимкнути JSON-кінець підписки незалежно."
"subTitle" = "Назва Підписки"
"subTitleDesc" = "Назва, яка відображається у VPN-клієнті"
"subSupportUrl" = "URL підтримки"
"subSupportUrlDesc" = "Посилання на технічну підтримку, що відображається у VPN-клієнті"
"subProfileUrl" = "URL профілю"
"subProfileUrlDesc" = "Посилання на ваш вебсайт, що відображається у VPN-клієнті"
"subAnnounce" = "Оголошення"
"subAnnounceDesc" = "Текст оголошення, що відображається у VPN-клієнті"
"subEnableRouting" = "Увімкнути маршрутизацію"
"subEnableRoutingDesc" = "Глобальне налаштування для увімкнення маршрутизації у VPN-клієнті. (Тільки для Happ)"
"subRoutingRules" = "Правила маршрутизації"
"subRoutingRulesDesc" = "Глобальні правила маршрутизації для VPN-клієнта. (Тільки для Happ)"
"subListen" = "Слухати IP"
"subListenDesc" = "IP-адреса для служби підписки. (залиште порожнім, щоб слухати всі IP-адреси)"
"subPort" = "Слухати порт"
@@ -531,6 +541,12 @@
"psk" = "Спільний ключ"
"domainStrategy" = "Стратегія домену"
[pages.xray.tun]
"nameDesc" = "Назва інтерфейсу TUN. Значення за замовчуванням - 'xray0'"
"mtuDesc" = "Максимальна одиниця передачі. Максимальний розмір пакетів даних. Значення за замовчуванням - 1500"
"userLevel" = "Рівень користувача"
"userLevelDesc" = "Всі з'єднання, встановлені через цей вхід, використовуватимуть цей рівень користувача. Значення за замовчуванням - 0"
[pages.xray.dns]
"enable" = "Увімкнути DNS"
"enableDesc" = "Увімкнути вбудований DNS-сервер"

View File

@@ -374,6 +374,16 @@
"subJsonEnable" = "Bật/Tắt điểm cuối đăng ký JSON độc lập."
"subTitle" = "Tiêu đề Đăng ký"
"subTitleDesc" = "Tiêu đề hiển thị trong ứng dụng VPN"
"subSupportUrl" = "URL Hỗ trợ"
"subSupportUrlDesc" = "Liên kết hỗ trợ kỹ thuật hiển thị trong ứng dụng VPN"
"subProfileUrl" = "URL Hồ sơ"
"subProfileUrlDesc" = "Liên kết đến trang web của bạn hiển thị trong ứng dụng VPN"
"subAnnounce" = "Thông báo"
"subAnnounceDesc" = "Văn bản thông báo hiển thị trong ứng dụng VPN"
"subEnableRouting" = "Bật định tuyến"
"subEnableRoutingDesc" = "Cài đặt toàn cục để bật định tuyến trong ứng dụng khách VPN. (Chỉ dành cho Happ)"
"subRoutingRules" = "Quy tắc định tuyến"
"subRoutingRulesDesc" = "Quy tắc định tuyến toàn cầu cho client VPN. (Chỉ dành cho Happ)"
"subListen" = "Listening IP"
"subListenDesc" = "Mặc định để trống để nghe tất cả các IP"
"subPort" = "Cổng gói đăng ký"
@@ -531,6 +541,12 @@
"psk" = "Khóa chia sẻ"
"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]
"enable" = "Kích hoạt DNS"
"enableDesc" = "Kích hoạt máy chủ DNS tích hợp"

View File

@@ -374,6 +374,16 @@
"subJsonEnable" = "单独启用/禁用 JSON 订阅端点。"
"subTitle" = "订阅标题"
"subTitleDesc" = "在VPN客户端中显示的标题"
"subSupportUrl" = "支持链接"
"subSupportUrlDesc" = "VPN 客户端中显示的技术支持链接"
"subProfileUrl" = "个人资料链接"
"subProfileUrlDesc" = "VPN 客户端中显示的网站链接"
"subAnnounce" = "公告"
"subAnnounceDesc" = "VPN 客户端中显示的公告文本"
"subEnableRouting" = "启用路由"
"subEnableRoutingDesc" = "在 VPN 客户端中启用路由的全局设置。(僅限 Happ"
"subRoutingRules" = "路由規則"
"subRoutingRulesDesc" = "VPN 用戶端的全域路由規則。(僅限 Happ"
"subListen" = "监听 IP"
"subListenDesc" = "订阅服务监听的 IP 地址(留空表示监听所有 IP"
"subPort" = "监听端口"
@@ -531,6 +541,12 @@
"psk" = "共享密钥"
"domainStrategy" = "域策略"
[pages.xray.tun]
"nameDesc" = "TUN 接口的名称。默认值为 'xray0'"
"mtuDesc" = "最大传输单元。数据包的最大大小。默认值为 1500"
"userLevel" = "用户级别"
"userLevelDesc" = "通过此入站的所有连接都将使用此用户级别。默认值为 0"
[pages.xray.dns]
"enable" = "启用 DNS"
"enableDesc" = "启用内置 DNS 服务器"

View File

@@ -374,6 +374,16 @@
"subJsonEnable" = "獨立啟用/停用 JSON 訂閱端點。"
"subTitle" = "訂閱標題"
"subTitleDesc" = "在VPN客戶端中顯示的標題"
"subSupportUrl" = "支援連結"
"subSupportUrlDesc" = "VPN 用戶端中顯示的技術支援連結"
"subProfileUrl" = "個人資料連結"
"subProfileUrlDesc" = "VPN 用戶端中顯示的網站連結"
"subAnnounce" = "公告"
"subAnnounceDesc" = "VPN 用戶端中顯示的公告文字"
"subEnableRouting" = "啟用路由"
"subEnableRoutingDesc" = "在 VPN 用戶端中啟用路由的全域設定。(僅限 Happ"
"subRoutingRules" = "路由規則"
"subRoutingRulesDesc" = "VPN 用戶端的全域路由規則。(僅限 Happ"
"subListen" = "監聽 IP"
"subListenDesc" = "訂閱服務監聽的 IP 地址(留空表示監聽所有 IP"
"subPort" = "監聽埠"
@@ -531,6 +541,12 @@
"psk" = "共享金鑰"
"domainStrategy" = "域策略"
[pages.xray.tun]
"nameDesc" = "TUN 介面的名稱。預設值為 'xray0'"
"mtuDesc" = "最大傳輸單元。資料包的最大大小。預設值為 1500"
"userLevel" = "用戶級別"
"userLevelDesc" = "通過此入站的所有連接都將使用此用戶級別。預設值為 0"
[pages.xray.dns]
"enable" = "啟用 DNS"
"enableDesc" = "啟用內建 DNS 伺服器"

View File

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

View File

@@ -20,12 +20,13 @@ const (
MessageTypeInbounds MessageType = "inbounds" // Inbounds list update
MessageTypeNotification MessageType = "notification" // System notification
MessageTypeXrayState MessageType = "xray_state" // Xray state change
MessageTypeOutbounds MessageType = "outbounds" // Outbounds list update
)
// Message represents a WebSocket message
type Message struct {
Type MessageType `json:"type"`
Payload interface{} `json:"payload"`
Payload any `json:"payload"`
Time int64 `json:"time"`
}
@@ -249,7 +250,7 @@ func (h *Hub) broadcastParallel(clients []*Client, message []byte) {
}
// 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 {
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
func (h *Hub) BroadcastToTopic(messageType MessageType, payload interface{}) {
func (h *Hub) BroadcastToTopic(messageType MessageType, payload any) {
if h == nil {
return
}

View File

@@ -25,7 +25,7 @@ func GetHub() *Hub {
}
// BroadcastStatus broadcasts server status update to all connected clients
func BroadcastStatus(status interface{}) {
func BroadcastStatus(status any) {
hub := GetHub()
if hub != nil {
hub.Broadcast(MessageTypeStatus, status)
@@ -33,7 +33,7 @@ func BroadcastStatus(status interface{}) {
}
// BroadcastTraffic broadcasts traffic statistics update to all connected clients
func BroadcastTraffic(traffic interface{}) {
func BroadcastTraffic(traffic any) {
hub := GetHub()
if hub != nil {
hub.Broadcast(MessageTypeTraffic, traffic)
@@ -41,13 +41,21 @@ func BroadcastTraffic(traffic interface{}) {
}
// BroadcastInbounds broadcasts inbounds list update to all connected clients
func BroadcastInbounds(inbounds interface{}) {
func BroadcastInbounds(inbounds any) {
hub := GetHub()
if hub != nil {
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
func BroadcastNotification(title, message, level string) {
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

389
x-ui.sh
View File

@@ -19,6 +19,23 @@ function LOGI() {
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
is_ipv4() {
[[ "$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_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
@@ -212,9 +229,9 @@ reset_user() {
read -rp "Do you want to disable currently configured two-factor authentication? (y/n): " twoFactorConfirm
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
${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."
fi
@@ -230,57 +247,6 @@ gen_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() {
echo -e "${yellow}Resetting Web Base Path${plain}"
@@ -340,16 +306,19 @@ check_config() {
fi
else
echo -e "${red}⚠ WARNING: No SSL certificate configured!${plain}"
read -rp "Generate a self-signed SSL certificate now? [y/N]: " gen_self
if [[ "$gen_self" == "y" || "$gen_self" == "Y" ]]; then
echo -e "${yellow}You can get a Let's Encrypt certificate for your IP address (valid ~6 days, auto-renews).${plain}"
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
setup_self_signed_certificate "${server_ip}"
ssl_cert_issue_for_ip
if [[ $? -eq 0 ]]; then
restart >/dev/null 2>&1
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
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}"
start >/dev/null 2>&1
fi
else
echo -e "${yellow}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}"
@@ -561,20 +530,27 @@ bbr_menu() {
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}"
before_show_menu
fi
# Replace BBR with CUBIC configurations
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
if [ -f "/etc/sysctl.d/99-bbr-x-ui.conf" ]; then
old_settings=$(head -1 /etc/sysctl.d/99-bbr-x-ui.conf | tr -d '#')
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
sysctl -p
# Verify that BBR is replaced with CUBIC
if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "cubic" ]]; then
if [[ $(sysctl -n net.ipv4.tcp_congestion_control) != "bbr" ]]; then
echo -e "${green}BBR has been replaced with CUBIC successfully.${plain}"
else
echo -e "${red}Failed to replace BBR with CUBIC. Please check your system configuration.${plain}"
@@ -582,50 +558,34 @@ disable_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}"
before_show_menu
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
echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf
# Apply changes
sysctl -p
if [ -d "/etc/sysctl.d/" ]; then
{
echo "#$(sysctl -n net.core.default_qdisc):$(sysctl -n net.ipv4.tcp_congestion_control)"
echo "net.core.default_qdisc = fq"
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
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}"
else
echo -e "${red}Failed to enable BBR. Please check your system configuration.${plain}"
@@ -951,24 +911,23 @@ delete_ports() {
}
update_all_geofiles() {
update_main_geofiles
update_ir_geofiles
update_ru_geofiles
update_geofiles "main"
update_geofiles "IR"
update_geofiles "RU"
}
update_main_geofiles() {
curl -fLRo geoip.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
curl -fLRo geosite.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
}
update_ir_geofiles() {
curl -fLRo geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
curl -fLRo geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
}
update_ru_geofiles() {
curl -fLRo geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
curl -fLRo geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
update_geofiles() {
case "${1}" in
"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";;
esac
for dat in "${dat_files[@]}"; do
# 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 \
https://github.com/${dat_source}/releases/latest/download/${remote_file}.dat
done
}
update_geo() {
@@ -979,24 +938,22 @@ update_geo() {
echo -e "${green}\t0.${plain} Back to Main Menu"
read -rp "Choose an option: " choice
cd ${xui_folder}/bin
case "$choice" in
0)
show_menu
;;
1)
update_main_geofiles
update_geofiles "main"
echo -e "${green}Loyalsoldier datasets have been updated successfully!${plain}"
restart
;;
2)
update_ir_geofiles
update_geofiles "IR"
echo -e "${green}chocolate4u datasets have been updated successfully!${plain}"
restart
;;
3)
update_ru_geofiles
update_geofiles "RU"
echo -e "${green}runetfreedom datasets have been updated successfully!${plain}"
restart
;;
@@ -1036,12 +993,12 @@ install_acme() {
}
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}\t3.${plain} Force Renew"
echo -e "${green}\t4.${plain} Show Existing Domains"
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"
read -rp "Choose an option: " choice
@@ -1136,9 +1093,10 @@ ssl_cert_issue_main() {
ssl_cert_issue_main
;;
6)
echo -e "${yellow}Automatic SSL Certificate for Server IP${plain}"
echo -e "This will automatically obtain and configure an SSL certificate for your server's IP address."
echo -e "${yellow}Note: Let's Encrypt supports IP certificates. Make sure port 80 is open.${plain}"
echo -e "${yellow}Let's Encrypt SSL Certificate for IP Address${plain}"
echo -e "This will obtain a certificate for your server's IP using the shortlived profile."
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"
if [[ $? == 0 ]]; then
ssl_cert_issue_for_ip
@@ -1155,6 +1113,7 @@ ssl_cert_issue_main() {
ssl_cert_issue_for_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_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}"
# 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
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
LOGI "acme.sh not found, installing..."
@@ -1211,66 +1175,114 @@ ssl_cert_issue_for_ip() {
;;
esac
# check if certificate already exists for this IP
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
if [ "${currentCert}" == "${server_ip}" ]; then
LOGI "Certificate already exists for IP: ${server_ip}"
certPath="/root/cert/${server_ip}"
else
# create directory for certificate
certPath="/root/cert/${server_ip}"
if [ ! -d "$certPath" ]; then
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/*
# Create certificate directory
certPath="/root/cert/ip"
mkdir -p "$certPath"
# Build domain arguments
local domain_args="-d ${server_ip}"
if [[ -n "$ipv6_addr" ]] && is_ipv6 "$ipv6_addr"; then
domain_args="${domain_args} -d ${ipv6_addr}"
LOGI "Including IPv6 address: ${ipv6_addr}"
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 --force
~/.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
local webCertFile="/root/cert/${server_ip}/fullchain.pem"
local webKeyFile="/root/cert/${server_ip}/privkey.pem"
local webCertFile="${certPath}/fullchain.pem"
local webKeyFile="${certPath}/privkey.pem"
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile"
LOGI "Certificate configured for panel"
LOGI " - Certificate File: $webCertFile"
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}"
LOGI "Panel will restart to apply SSL certificate..."
restart
@@ -1379,7 +1391,7 @@ ssl_cert_issue() {
LOGI "Will use port: ${WebPort} to issue certificates. Please make sure this port is open."
# issue the certificate
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force
if [ $? -ne 0 ]; then
LOGE "Issuing certificate failed, please check logs."
@@ -1433,12 +1445,14 @@ ssl_cert_issue() {
if [ $? -ne 0 ]; then
LOGE "Auto renew failed, certificate details:"
ls -lah cert/*
chmod 755 $certPath/*
chmod 600 $certPath/privkey.pem
chmod 644 $certPath/fullchain.pem
exit 1
else
LOGI "Auto renew succeeded, certificate details:"
ls -lah cert/*
chmod 755 $certPath/*
chmod 600 $certPath/privkey.pem
chmod 644 $certPath/fullchain.pem
fi
# Prompt user to set panel paths after successful certificate installation
@@ -1504,7 +1518,7 @@ ssl_cert_issue_CF() {
LOGD "Your registered email address is: ${CF_AccountEmail}"
# Set the default CA to Let's Encrypt
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force
if [ $? -ne 0 ]; then
LOGE "Default CA, Let'sEncrypt fail, script exiting..."
exit 1
@@ -1578,7 +1592,8 @@ ssl_cert_issue_CF() {
else
LOGI "The certificate is installed and auto-renewal is turned on. Specific information is as follows:"
ls -lah ${certPath}/*
chmod 755 ${certPath}/*
chmod 600 ${certPath}/privkey.pem
chmod 644 ${certPath}/fullchain.pem
fi
# 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
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))
for i, v := range testseedArr {
if num, ok := v.(float64); ok {