1
0

10 Коммиты 8098d2b1b1 ... d8c783a296

Автор SHA1 Сообщение Дата
  MHSanaei d8c783a296 v2.8.8 1 неделя назад
  MHSanaei 809f69729a Update minimum Xray version requirement 1 неделя назад
  MHSanaei 93b7ce199f Add UDP mask support for Hysteria outbound 1 неделя назад
  MHSanaei 2a76cec804 Add Hysteria2 outbound protocol support 1 неделя назад
  MHSanaei 88eab032be Add TUN protocol for inbound 1 неделя назад
  MHSanaei 20ec863f51 Xray Core v26.1.18 1 неделя назад
  Nebulosa 2f4018bbe5 feat: improve BBR management with sysctl.d and backup support (#3658) 1 неделя назад
  Vorontsov Amadey f273708f6d Feature: Use of username and passwords consisting of several words (#3647) 1 неделя назад
  Nebulosa e6318d57e4 Add x-ui.service.arch file (#3650) 1 неделя назад
  lolka1333 77fa976ee9 Enhance WebSocket client connection logic and improve event listener management (#3636) 1 неделя назад

+ 5 - 3
.github/workflows/release.yml

@@ -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.18/"
           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.18/"
           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

+ 1 - 1
DockerInit.sh

@@ -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.18/Xray-linux-${ARCH}.zip"
 unzip "Xray-linux-${ARCH}.zip"
 rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
 mv xray "xray-linux-${FNAME}"

+ 1 - 1
config/version

@@ -1 +1 @@
-2.8.7
+2.8.8

+ 18 - 16
go.mod

@@ -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,7 +11,7 @@ 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
@@ -20,11 +20,11 @@ require (
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	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.260118.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,6 +33,7 @@ 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
@@ -47,7 +48,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,20 +58,20 @@ 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.70 // 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/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/quic-go/quic-go v0.59.0 // indirect
+	github.com/refraction-networking/utls v1.8.2 // indirect
 	github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
 	github.com/rogpeppe/go-internal v1.14.1 // indirect
 	github.com/sagernet/sing v0.7.14 // indirect
@@ -89,15 +90,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-20260114163908-3f89685c29c3 // indirect
 	google.golang.org/protobuf v1.36.11 // indirect
-	gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
+	gvisor.dev/gvisor v0.0.0-20260109181451-4be7c433dae2 // indirect
 	lukechampine.com/blake3 v1.4.1 // indirect
 )

+ 34 - 30
go.sum

@@ -6,6 +6,8 @@ github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktp
 github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
 github.com/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=
@@ -57,8 +59,8 @@ github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy0
 github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
 github.com/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 +108,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/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 +124,15 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-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.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
+github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-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=
@@ -147,10 +149,10 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
 github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
 github.com/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/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/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/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
@@ -203,8 +205,8 @@ github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
 github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
 github.com/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.260118.0 h1:RJtgIbQ3ykFRcH1CKeoCgQ5WvhsMFu+lnvLF/fFHagE=
+github.com/xtls/xray-core v1.260118.0/go.mod h1:A5k7TXE2KfAjT8dAq6Ir4mMP1q0OTh+8VMmUdqWMQpg=
 github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
 github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
 github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
@@ -231,12 +233,14 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
 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 +249,22 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.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-20260114163908-3f89685c29c3 h1:C4WAdL+FbjnGlpp2S+HMVhBeCq2Lcib4xZqfPNF6OoQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
 google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
 google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
 google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
@@ -278,7 +282,7 @@ gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
 gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
 gorm.io/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-20260109181451-4be7c433dae2 h1:fr6L00yGG2RP5NMea6njWpdC+bm+cMdFClrSpaicp1c=
+gvisor.dev/gvisor v0.0.0-20260109181451-4be7c433dae2/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
 lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
 lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=

+ 12 - 0
install.sh

@@ -818,6 +818,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}"
@@ -837,6 +846,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
                 ;;

+ 13 - 0
update.sh

@@ -737,6 +737,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
@@ -819,6 +820,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}"
@@ -837,6 +847,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
                     ;;

+ 34 - 0
web/assets/js/model/inbound.js

@@ -7,6 +7,7 @@ const Protocols = {
     MIXED: 'mixed',
     HTTP: 'http',
     WIREGUARD: 'wireguard',
+    TUN: 'tun',
 };
 
 const SSMethods = {
@@ -1739,6 +1740,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 +1755,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;
         }
     }
@@ -2586,3 +2589,34 @@ Inbound.WireguardSettings.Peer = class extends XrayCommonClass {
         };
     }
 };
+
+Inbound.TunSettings = class extends Inbound.Settings {
+    constructor(
+        protocol,
+        name = 'xray0',
+        mtu = 1500,
+        userLevel = 0
+    ) {
+        super(protocol);
+        this.name = name;
+        this.mtu = mtu;
+        this.userLevel = userLevel;
+    }
+
+    static fromJson(json = {}) {
+        return new Inbound.TunSettings(
+            Protocols.TUN,
+            json.name ?? 'xray0',
+            json.mtu ?? json.MTU ?? 1500,
+            json.userLevel ?? 0
+        );
+    }
+
+    toJson() {
+        return {
+            name: this.name || 'xray0',
+            mtu: this.mtu || 1500,
+            userLevel: this.userLevel || 0,
+        };
+    }
+};

+ 219 - 4
web/assets/js/model/outbound.js

@@ -8,7 +8,8 @@ const Protocols = {
     Shadowsocks: "shadowsocks",
     Socks: "socks",
     HTTP: "http",
-    Wireguard: "wireguard"
+    Wireguard: "wireguard",
+    Hysteria: "hysteria"
 };
 
 const SSMethods = {
@@ -424,6 +425,90 @@ class RealityStreamSettings extends CommonClass {
         };
     }
 };
+
+class HysteriaStreamSettings extends CommonClass {
+    constructor(
+        version = 2,
+        auth = '',
+        congestion = '',
+        up = '0',
+        down = '0',
+        udphopPort = '',
+        udphopInterval = 30,
+        initStreamReceiveWindow = 8388608,
+        maxStreamReceiveWindow = 8388608,
+        initConnectionReceiveWindow = 20971520,
+        maxConnectionReceiveWindow = 20971520,
+        maxIdleTimeout = 30,
+        keepAlivePeriod = 0,
+        disablePathMTUDiscovery = false
+    ) {
+        super();
+        this.version = version;
+        this.auth = auth;
+        this.congestion = congestion;
+        this.up = up;
+        this.down = down;
+        this.udphopPort = udphopPort;
+        this.udphopInterval = udphopInterval;
+        this.initStreamReceiveWindow = initStreamReceiveWindow;
+        this.maxStreamReceiveWindow = maxStreamReceiveWindow;
+        this.initConnectionReceiveWindow = initConnectionReceiveWindow;
+        this.maxConnectionReceiveWindow = maxConnectionReceiveWindow;
+        this.maxIdleTimeout = maxIdleTimeout;
+        this.keepAlivePeriod = keepAlivePeriod;
+        this.disablePathMTUDiscovery = disablePathMTUDiscovery;
+    }
+
+    static fromJson(json = {}) {
+        let udphopPort = '';
+        let udphopInterval = 30;
+        if (json.udphop) {
+            udphopPort = json.udphop.port || '';
+            udphopInterval = json.udphop.interval || 30;
+        }
+        return new HysteriaStreamSettings(
+            json.version,
+            json.auth,
+            json.congestion,
+            json.up,
+            json.down,
+            udphopPort,
+            udphopInterval,
+            json.initStreamReceiveWindow,
+            json.maxStreamReceiveWindow,
+            json.initConnectionReceiveWindow,
+            json.maxConnectionReceiveWindow,
+            json.maxIdleTimeout,
+            json.keepAlivePeriod,
+            json.disablePathMTUDiscovery
+        );
+    }
+
+    toJson() {
+        const result = {
+            version: this.version,
+            auth: this.auth,
+            congestion: this.congestion,
+            up: this.up,
+            down: this.down,
+            initStreamReceiveWindow: this.initStreamReceiveWindow,
+            maxStreamReceiveWindow: this.maxStreamReceiveWindow,
+            initConnectionReceiveWindow: this.initConnectionReceiveWindow,
+            maxConnectionReceiveWindow: this.maxConnectionReceiveWindow,
+            maxIdleTimeout: this.maxIdleTimeout,
+            keepAlivePeriod: this.keepAlivePeriod,
+            disablePathMTUDiscovery: this.disablePathMTUDiscovery
+        };
+        if (this.udphopPort) {
+            result.udphop = {
+                port: this.udphopPort,
+                interval: this.udphopInterval
+            };
+        }
+        return result;
+    }
+};
 class SockoptStreamSettings extends CommonClass {
     constructor(
         dialerProxy = "",
@@ -473,6 +558,30 @@ class SockoptStreamSettings extends CommonClass {
     }
 }
 
+class UdpMask extends CommonClass {
+    constructor(type = 'salamander', password = '') {
+        super();
+        this.type = type;
+        this.password = password;
+    }
+
+    static fromJson(json = {}) {
+        return new UdpMask(
+            json.type,
+            json.settings?.password || ''
+        );
+    }
+
+    toJson() {
+        return {
+            type: this.type,
+            settings: {
+                password: this.password
+            }
+        };
+    }
+}
+
 class StreamSettings extends CommonClass {
     constructor(
         network = 'tcp',
@@ -485,6 +594,8 @@ class StreamSettings extends CommonClass {
         grpcSettings = new GrpcStreamSettings(),
         httpupgradeSettings = new HttpUpgradeStreamSettings(),
         xhttpSettings = new xHTTPStreamSettings(),
+        hysteriaSettings = new HysteriaStreamSettings(),
+        udpmasks = [],
         sockopt = undefined,
     ) {
         super();
@@ -498,9 +609,19 @@ class StreamSettings extends CommonClass {
         this.grpc = grpcSettings;
         this.httpupgrade = httpupgradeSettings;
         this.xhttp = xhttpSettings;
+        this.hysteria = hysteriaSettings;
+        this.udpmasks = udpmasks;
         this.sockopt = sockopt;
     }
 
+    addUdpMask() {
+        this.udpmasks.push(new UdpMask());
+    }
+
+    delUdpMask(index) {
+        this.udpmasks.splice(index, 1);
+    }
+
     get isTls() {
         return this.security === 'tls';
     }
@@ -518,6 +639,7 @@ class StreamSettings extends CommonClass {
     }
 
     static fromJson(json = {}) {
+        const udpmasks = json.udpmasks ? json.udpmasks.map(mask => UdpMask.fromJson(mask)) : [];
         return new StreamSettings(
             json.network,
             json.security,
@@ -529,6 +651,8 @@ class StreamSettings extends CommonClass {
             GrpcStreamSettings.fromJson(json.grpcSettings),
             HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
             xHTTPStreamSettings.fromJson(json.xhttpSettings),
+            HysteriaStreamSettings.fromJson(json.hysteriaSettings),
+            udpmasks,
             SockoptStreamSettings.fromJson(json.sockopt),
         );
     }
@@ -546,6 +670,8 @@ 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,
+            udpmasks: this.udpmasks.length > 0 ? this.udpmasks.map(mask => mask.toJson()) : undefined,
             sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
         };
     }
@@ -609,7 +735,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 +761,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 +800,8 @@ class Outbound extends CommonClass {
             Protocols.Trojan,
             Protocols.Shadowsocks,
             Protocols.Socks,
-            Protocols.HTTP
+            Protocols.HTTP,
+            Protocols.Hysteria
         ].includes(this.protocol);
     }
 
@@ -722,6 +850,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 +973,62 @@ 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') ?? '';
+        stream.hysteria.udphopInterval = parseInt(urlParams.get('udphopInterval') ?? '30');
+
+        // Optional QUIC parameters
+        if (urlParams.has('initStreamReceiveWindow')) {
+            stream.hysteria.initStreamReceiveWindow = parseInt(urlParams.get('initStreamReceiveWindow'));
+        }
+        if (urlParams.has('maxStreamReceiveWindow')) {
+            stream.hysteria.maxStreamReceiveWindow = parseInt(urlParams.get('maxStreamReceiveWindow'));
+        }
+        if (urlParams.has('initConnectionReceiveWindow')) {
+            stream.hysteria.initConnectionReceiveWindow = parseInt(urlParams.get('initConnectionReceiveWindow'));
+        }
+        if (urlParams.has('maxConnectionReceiveWindow')) {
+            stream.hysteria.maxConnectionReceiveWindow = parseInt(urlParams.get('maxConnectionReceiveWindow'));
+        }
+        if (urlParams.has('maxIdleTimeout')) {
+            stream.hysteria.maxIdleTimeout = parseInt(urlParams.get('maxIdleTimeout'));
+        }
+        if (urlParams.has('keepAlivePeriod')) {
+            stream.hysteria.keepAlivePeriod = parseInt(urlParams.get('keepAlivePeriod'));
+        }
+        if (urlParams.has('disablePathMTUDiscovery')) {
+            stream.hysteria.disablePathMTUDiscovery = urlParams.get('disablePathMTUDiscovery') === 'true';
+        }
+
+        // Create settings
+        let settings = new Outbound.HysteriaSettings(address, port, 2);
+
+        // Extract remark from hash
+        let remark = hash ? decodeURIComponent(hash.substring(1)) : `out-hysteria-${port}`;
+
+        return new Outbound(remark, Protocols.Hysteria, settings, stream);
+    }
 }
 
 Outbound.Settings = class extends CommonClass {
@@ -862,6 +1049,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 +1066,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;
         }
     }
@@ -1324,4 +1513,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
+        };
+    }
 };

+ 7 - 2
web/assets/js/websocket.js

@@ -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) {

+ 44 - 18
web/html/form/inbound.html

@@ -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"}}

+ 296 - 100
web/html/form/outbound.html

@@ -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'">
@@ -343,7 +408,8 @@
         <!-- 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 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>
@@ -357,25 +423,31 @@
             <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 +460,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 +497,144 @@
             <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 (s)'
+            v-if="outbound.stream.hysteria.udphopPort">
+            <a-input-number
+              v-model.number="outbound.stream.hysteria.udphopInterval"
+              :min="5"></a-input-number>
+          </a-form-item>
+          <a-form-item label='Init Stream Receive'>
+            <a-input-number
+              v-model.number="outbound.stream.hysteria.initStreamReceiveWindow"></a-input-number>
+          </a-form-item>
+          <a-form-item label='Max Stream Receive'>
+            <a-input-number
+              v-model.number="outbound.stream.hysteria.maxStreamReceiveWindow"></a-input-number>
+          </a-form-item>
+          <a-form-item label='Init Connection Receive'>
+            <a-input-number
+              v-model.number="outbound.stream.hysteria.initConnectionReceiveWindow"></a-input-number>
+          </a-form-item>
+          <a-form-item label='Max Connection Receive'>
+            <a-input-number
+              v-model.number="outbound.stream.hysteria.maxConnectionReceiveWindow"></a-input-number>
+          </a-form-item>
+          <a-form-item label='Max Idle Timeout (s)'>
+            <a-input-number
+              v-model.number="outbound.stream.hysteria.maxIdleTimeout" :min="4"
+              :max="120"></a-input-number>
+          </a-form-item>
+          <a-form-item label='Keep Alive Period (s)'>
+            <a-input-number
+              v-model.number="outbound.stream.hysteria.keepAlivePeriod" :min="0"
+              :max="60"></a-input-number>
+          </a-form-item>
+          <a-form-item label='Disable Path MTU'>
+            <a-switch
+              v-model="outbound.stream.hysteria.disablePathMTUDiscovery"></a-switch>
           </a-form-item>
         </template>
       </template>
 
+      <!-- udpmasks settings -->
+      <template v-if="outbound.canEnableStream()">
+        <a-form-item label="UDP Masks">
+          <a-button icon="plus" type="primary" size="small" @click="outbound.stream.addUdpMask()"></a-button>
+        </a-form-item>
+        <template v-if="outbound.stream.udpmasks.length > 0">
+          <a-form v-for="(mask, index) in outbound.stream.udpmasks" :key="index" :colon="false"
+            :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
+            <a-divider :style="{ margin: '0' }"> UDP Mask [[ index + 1 ]]
+              <a-icon type="delete" @click="() => outbound.stream.delUdpMask(index)"
+                :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
+            </a-divider>
+            <a-form-item label='Type'>
+              <a-select v-model="mask.type" :dropdown-class-name="themeSwitcher.currentTheme">
+                <a-select-option value="salamander">Salamander</a-select-option>
+              </a-select>
+            </a-form-item>
+            <a-form-item label='Password'>
+              <a-input v-model.trim="mask.password" placeholder="Obfuscation password"></a-input>
+            </a-form-item>
+          </a-form>
+        </template>
+      </template>
+
       <!-- 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 +642,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">
@@ -492,11 +668,14 @@
         <!-- 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 +685,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 +701,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 +729,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 +750,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 +770,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"
+      <a-input addon-before='{{ i18n "pages.xray.outbound.link" }}'
+        v-model.trim="outModal.link"
         placeholder="vmess:// vless:// trojan:// ss://">
         <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>

+ 44 - 0
web/html/form/protocol/tun.html

@@ -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}}

+ 24 - 8
web/html/inbounds.html

@@ -1602,7 +1602,6 @@
           if (payload && Array.isArray(payload)) {
             // Use setInbounds to properly convert to DBInbound objects with methods
             this.setInbounds(payload);
-            this.searchInbounds(this.searchKey);
           }
         });
 
@@ -1614,14 +1613,31 @@
           
           // 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

+ 39 - 17
web/html/settings/panel/subscription/subpage.html

@@ -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" .}}
@@ -138,27 +175,12 @@
                                 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)" style="
-                                cursor: pointer;
-                                background: rgba(0, 0, 0, 0.2);
-                                border: 1px solid rgba(255, 255, 255, 0.1);
-                                border-radius: 12px;
-                                padding: 25px 20px 15px 20px;
-                                margin-top: -12px;
-                                word-break: break-all;
-                                color: #fff;
-                                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);
-                            " onmouseover="this.style.background='rgba(0, 0, 0, 0.3)'; this.style.borderColor='rgba(255, 255, 255, 0.2)'"
-                                onmouseout="this.style.background='rgba(0, 0, 0, 0.2)'; this.style.borderColor='rgba(255, 255, 255, 0.1)'">
+                            <div @click="copy(link)" class="subscription-link-box">
                                 [[ link ]]
                             </div>
                         </div>
                     </div>
+
                     </div>
                     <br />
 

+ 1 - 1
web/service/server.go

@@ -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 >= 18) {
 			versions = append(versions, release.TagName)
 		}
 	}

+ 6 - 0
web/translation/translate.ar_EG.toml

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

+ 6 - 0
web/translation/translate.en_US.toml

@@ -531,6 +531,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"

+ 6 - 0
web/translation/translate.es_ES.toml

@@ -531,6 +531,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"

+ 6 - 0
web/translation/translate.fa_IR.toml

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

+ 6 - 0
web/translation/translate.id_ID.toml

@@ -531,6 +531,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"

+ 6 - 0
web/translation/translate.ja_JP.toml

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

+ 6 - 0
web/translation/translate.pt_BR.toml

@@ -531,6 +531,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"

+ 6 - 0
web/translation/translate.ru_RU.toml

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

+ 6 - 0
web/translation/translate.tr_TR.toml

@@ -531,6 +531,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"

+ 6 - 0
web/translation/translate.uk_UA.toml

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

+ 6 - 0
web/translation/translate.vi_VN.toml

@@ -531,6 +531,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"

+ 6 - 0
web/translation/translate.zh_CN.toml

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

+ 6 - 0
web/translation/translate.zh_TW.toml

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

+ 16 - 0
x-ui.service.arch

@@ -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

+ 39 - 18
x-ui.sh

@@ -229,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
     
@@ -530,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
-
-    # Apply changes
-    sysctl -p
+    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
 
-    # 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}"
@@ -551,20 +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
 
     # 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}"