Compare commits

..

1 Commits

Author SHA1 Message Date
mhsanaei
f26567d85d v2.3.15 2024-09-05 10:23:01 +02:00
127 changed files with 3868 additions and 5690 deletions

10
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "gomod" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

View File

@@ -1,57 +1,41 @@
name: Release 3X-UI for Docker
on:
workflow_dispatch:
push:
tags:
- "v*.*.*"
- "*"
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-20.04
build_and_push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
hsanaeii/3x-ui
ghcr.io/mhsanaei/3x-ui
tags: |
type=ref,event=branch
type=ref,event=tag
type=pep440,pattern={{version}}
- name: Check out the code
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
install: true
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64, linux/arm64/v8, linux/arm/v7, linux/arm/v6, linux/386
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64, linux/arm64/v8, linux/arm/v7, linux/arm/v6, linux/386
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,10 +1,10 @@
name: Release 3X-UI
on:
workflow_dispatch:
push:
tags:
- "v*.*.*"
- "*"
workflow_dispatch:
jobs:
build:
@@ -72,7 +72,7 @@ jobs:
export GOARCH=s390x
export CC=s390x-linux-gnu-gcc
fi
go build -ldflags "-w -s" -o xui-release -v main.go
go build -o xui-release -v main.go
mkdir x-ui
cp xui-release x-ui/
@@ -83,43 +83,43 @@ jobs:
cd x-ui/bin
# Download dependencies
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.1.30/"
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v1.8.24/"
if [ "${{ matrix.platform }}" == "amd64" ]; then
wget -q ${Xray_URL}Xray-linux-64.zip
wget ${Xray_URL}Xray-linux-64.zip
unzip Xray-linux-64.zip
rm -f Xray-linux-64.zip
elif [ "${{ matrix.platform }}" == "arm64" ]; then
wget -q ${Xray_URL}Xray-linux-arm64-v8a.zip
wget ${Xray_URL}Xray-linux-arm64-v8a.zip
unzip Xray-linux-arm64-v8a.zip
rm -f Xray-linux-arm64-v8a.zip
elif [ "${{ matrix.platform }}" == "armv7" ]; then
wget -q ${Xray_URL}Xray-linux-arm32-v7a.zip
wget ${Xray_URL}Xray-linux-arm32-v7a.zip
unzip Xray-linux-arm32-v7a.zip
rm -f Xray-linux-arm32-v7a.zip
elif [ "${{ matrix.platform }}" == "armv6" ]; then
wget -q ${Xray_URL}Xray-linux-arm32-v6.zip
wget ${Xray_URL}Xray-linux-arm32-v6.zip
unzip Xray-linux-arm32-v6.zip
rm -f Xray-linux-arm32-v6.zip
elif [ "${{ matrix.platform }}" == "386" ]; then
wget -q ${Xray_URL}Xray-linux-32.zip
wget ${Xray_URL}Xray-linux-32.zip
unzip Xray-linux-32.zip
rm -f Xray-linux-32.zip
elif [ "${{ matrix.platform }}" == "armv5" ]; then
wget -q ${Xray_URL}Xray-linux-arm32-v5.zip
wget ${Xray_URL}Xray-linux-arm32-v5.zip
unzip Xray-linux-arm32-v5.zip
rm -f Xray-linux-arm32-v5.zip
elif [ "${{ matrix.platform }}" == "s390x" ]; then
wget -q ${Xray_URL}Xray-linux-s390x.zip
wget ${Xray_URL}Xray-linux-s390x.zip
unzip Xray-linux-s390x.zip
rm -f Xray-linux-s390x.zip
fi
rm -f geoip.dat geosite.dat
wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget -q -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
wget -q -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
wget -q -O geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
wget -q -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
wget -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
wget -O geoip_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geoip.dat
wget -O geosite_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geosite.dat
mv xray xray-linux-${{ matrix.platform }}
cd ../..

View File

@@ -1,7 +1,7 @@
#!/bin/sh
# Start fail2ban
[ $X_UI_ENABLE_FAIL2BAN == "true" ] && fail2ban-client -x start
fail2ban-client -x start
# Run x-ui
exec /app/x-ui

View File

@@ -27,14 +27,14 @@ case $1 in
esac
mkdir -p build/bin
cd build/bin
wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.1.30/Xray-linux-${ARCH}.zip"
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.24/Xray-linux-${ARCH}.zip"
unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
mv xray "xray-linux-${FNAME}"
wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget -q -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
wget -q -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
wget -q -O geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
wget -q -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
wget -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
wget -O geoip_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geoip.dat
wget -O geosite_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geosite.dat
cd ../../

View File

@@ -15,7 +15,7 @@ COPY . .
ENV CGO_ENABLED=1
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
RUN go build -ldflags "-w -s" -o build/x-ui main.go
RUN go build -o build/x-ui main.go
RUN ./DockerInit.sh "$TARGETARCH"
# ========================================================
@@ -48,7 +48,6 @@ RUN chmod +x \
/app/x-ui \
/usr/bin/x-ui
ENV X_UI_ENABLE_FAIL2BAN="true"
VOLUME [ "/etc/x-ui" ]
CMD [ "./x-ui" ]
ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]

View File

@@ -1,11 +1,6 @@
[English](/README.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/3x-ui-dark.png">
<img alt="3x-ui" src="./media/3x-ui-light.png">
</picture>
</p>
<p align="center"><a href="#"><img src="./media/3X-UI.png" alt="Image"></a></p>
**Un Panel Web Avanzado • Construido sobre Xray Core**
@@ -35,62 +30,38 @@
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```
## Instalar versión antigua (no recomendamos)
## Instalar una Versión Personalizada
Para instalar la versión deseada, utiliza el siguiente comando de instalación. Por ejemplo, ver `v1.7.9`:
Para instalar la versión deseada, agrega la versión al final del comando de instalación. Por ejemplo, ver `v2.3.15`:
```
VERSION=v1.7.9 && <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/$VERSION/install.sh") $VERSION
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.3.15
```
## Certificado SSL
<details>
<summary>Haga clic para ver los detalles del certificado SSL</summary>
<summary>Haz clic para el Certificado SSL</summary>
### ACME
### Cloudflare
Para gestionar certificados SSL utilizando ACME:
El script de gestión tiene una aplicación de certificado SSL incorporada para Cloudflare. Para usar este script para colocar un certificado, necesitas lo siguiente:
1. Asegúrate de que tu dominio esté correctamente resuelto al servidor.
2. Ejecuta el comando `x-ui` en la terminal y elige `Gestión de Certificados SSL`.
3. Se te presentarán las siguientes opciones:
- Correo electrónico registrado en Cloudflare
- Clave Global de API de Cloudflare
- El nombre de dominio se ha resuelto en el servidor actual a través de Cloudflare
**1:** Ejecuta el comando`x-ui`en la terminal, luego elige `Certificado SSL de Cloudflare`.
- **Get SSL:** Obtener certificados SSL.
- **Revoke:** Revocar certificados SSL existentes.
- **Force Renew:** Forzar la renovación de certificados SSL.
- **Show Existing Domains:** Mostrar todos los certificados de dominio disponibles en el servidor.
- **Set Certificate Paths for the Panel:** Especificar el certificado para tu dominio que será utilizado por el panel.
### Certbot
Para instalar y usar Certbot:
```sh
```
apt-get install certbot -y
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
certbot renew --dry-run
```
### Cloudflare
El script de gestión incluye una aplicación de certificado SSL integrada para Cloudflare. Para usar este script para solicitar un certificado, necesitas lo siguiente:
- Correo electrónico registrado en Cloudflare
- Clave API Global de Cloudflare
- El nombre de dominio debe estar resuelto al servidor actual a través de Cloudflare
**Cómo obtener la Clave API Global de Cloudflare:**
1. Ejecuta el comando `x-ui` en la terminal y elige `Certificado SSL de Cloudflare`.
2. Visita el enlace: [Tokens de API de Cloudflare](https://dash.cloudflare.com/profile/api-tokens).
3. Haz clic en "Ver Clave API Global" (consulta la captura de pantalla a continuación):
![](media/APIKey1.PNG)
4. Es posible que necesites volver a autenticar tu cuenta. Después de eso, se mostrará la Clave API (consulta la captura de pantalla a continuación):
![](media/APIKey2.png)
Al utilizarlo, simplemente ingresa tu `nombre de dominio`, `correo electrónico` y `CLAVE API`. El diagrama es el siguiente:
![](media/DetailEnter.png)
***Consejo:*** *Certbot también está integrado en el script de gestión. Puedes ejecutar el comando `x-ui` , luego elegir `Gestión de Certificados SSL`.*
</details>
@@ -247,18 +218,13 @@ location /sub {
- Ubuntu 20.04+
- Debian 11+
- CentOS 8+
- OpenEuler 22.03+
- Fedora 36+
- Arch Linux
- Parch Linux
- Manjaro
- Armbian
- AlmaLinux 8.0+
- Rocky Linux 8+
- Oracle Linux 8+
- AlmaLinux 9+
- Rockylinux 9+
- OpenSUSE Tubleweed
- Amazon Linux 2023
- Windows x64
## Arquitecturas y Dispositivos Compatibles
@@ -282,18 +248,14 @@ Nuestra plataforma ofrece compatibilidad con una amplia gama de arquitecturas y
## Idiomas
- English (inglés)
- Persian (persa)
- Traditional Chinese (chino tradicional)
- Simplified Chinese (chino simplificado)
- Japanese (japonés)
- Russian (ruso)
- Vietnamese (vietnamita)
- Spanish (español)
- Indonesian (indonesio)
- Ukrainian (ucraniano)
- Turkish (turco)
- Português (Brazil) (portugués (Brasil))
- Inglés
- Farsi
- Chino
- Ruso
- Vietnamita
- Español
- Indonesio
- Ucraniano
## Características
@@ -314,103 +276,88 @@ Nuestra plataforma ofrece compatibilidad con una amplia gama de arquitecturas y
- Soporta exportar/importar base de datos desde el panel
## Configuración Predeterminada del Panel
## Configuraciones por Defecto
<details>
<summary>Haz clic para ver los detalles de la configuración predeterminada</summary>
<summary>Haz clic para detalles de las configuraciones por defecto</summary>
### Nombre de usuario, Contraseña, Puerto y Ruta Base Web
### Información
Si elige no modificar estas configuraciones, se generarán aleatoriamente (esto no se aplica a Docker).
**Configuraciones predeterminadas para Docker:**
- **Nombre de usuario:** admin
- **Contraseña:** admin
- **Puerto:** 2053
### Gestión de la Base de Datos:
Puedes realizar copias de seguridad y restauraciones de la base de datos directamente desde el panel.
- **Usuario y Contraseña:** Se generarán aleatoriamente si omites la modificación.
- **Ruta de la Base de Datos:**
- `/etc/x-ui/x-ui.db`
### Ruta Base Web
1. **Restablecer la Ruta Base Web:**
- Abre tu terminal.
- Ejecuta el comando `x-ui`.
- Selecciona la opción `Restablecer la Ruta Base Web`.
2. **Generar o Personalizar la Ruta:**
- La ruta se generará aleatoriamente, o puedes ingresar una ruta personalizada.
3. **Ver Configuración Actual:**
- Para ver tu configuración actual, utiliza el comando `x-ui settings` en el terminal o selecciona `Ver Configuración Actual` en `x-ui`.
### Recomendación de Seguridad:
- Para mayor seguridad, utiliza una palabra larga y aleatoria en la estructura de tu URL.
**Ejemplos:**
- `http://ip:port/*webbasepath*/panel`
- `http://domain:port/*webbasepath*/panel`
- /etc/x-ui/x-ui.db
- **Ruta de Configuración de Xray:**
- /usr/local/x-ui/bin/config.json
- **Ruta del Panel Web sin Implementar SSL:**
- http://ip:2053/panel
- http://domain:2053/panel
- **Ruta del Panel Web con Implementación de SSL:**
- https://domain:2053/panel
</details>
## Configuración de WARP
## Configuración WARP
<details>
<summary>Haz clic para ver los detalles de la configuración de WARP</summary>
<summary>Haz clic para detalles de la configuración WARP</summary>
#### Uso
**Para versiones `v2.1.0` y posteriores:**
Si deseas usar enrutamiento a WARP antes de la versión v2.1.0, sigue los pasos a continuación:
WARP está integrado, no se requiere instalación adicional. Simplemente habilita la configuración necesaria en el panel.
**1.** Instala WARP en **Modo de Proxy SOCKS**:
```sh
bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh)
```
**2.** Si ya instalaste warp, puedes desinstalarlo usando el siguiente comando:
```sh
warp u
```
**3.** Activa la configuración que necesites en el panel
Características de Configuración:
- Bloquear Anuncios
- Enrutar Google + Netflix + Spotify + OpenAI (ChatGPT) a WARP
- Corregir error 403 de Google
</details>
## Límite de IP
<details>
<summary>Haz clic para ver los detalles del límite de IP</summary>
<summary>Haz clic para s detalles del límite de IP</summary>
#### Uso
**Nota:** El Límite de IP no funcionará correctamente cuando uses Túnel IP.
**Nota:** El Límite de IP no funcionará correctamente cuando se use IP Tunnel
- Para versiones hasta `v1.6.1`:
- **Para versiones hasta `v1.6.1`:**
- El límite de IP está integrado en el panel.
**Para versiones `v1.7.0` y posteriores:**
- Para versiones `v1.7.0` y posteriores:
Para habilitar la funcionalidad de límite de IP, necesitas instalar `fail2ban` y los archivos requeridos siguiendo estos pasos:
- Para que el Límite de IP funcione correctamente, necesitas instalar fail2ban y sus archivos requeridos siguiendo estos pasos:
1. Ejecuta el comando `x-ui` en el terminal, luego elige `Gestión de Límite de IP`.
2. Verás las siguientes opciones:
1. Usa el comando `x-ui` dentro de la terminal.
2. Selecciona `Gestión de Límite de IP`.
3. Elige las opciones apropiadas según tus necesidades.
- **Cambiar la Duración del Bloqueo:** Ajustar la duración de los bloqueos.
- **Desbloquear a Todos:** Levantar todos los bloqueos actuales.
- **Revisar los Registros:** Revisar los registros.
- **Estado de Fail2ban:** Verificar el estado de `fail2ban`.
- **Reiniciar Fail2ban:** Reiniciar el servicio `fail2ban`.
- **Desinstalar Fail2ban:** Desinstalar Fail2ban con la configuración.
- asegúrate de tener ./access.log en tu Configuración de Xray después de la v2.1.3 tenemos una opción para ello
3. Agrega una ruta para el registro de acceso en el panel configurando `Xray Configs/log/Access log` a `./access.log`, luego guarda y reinicia Xray.
- **Para versiones anteriores a `v2.1.3`:**
- Necesitas configurar manualmente la ruta del registro de acceso en tu configuración de Xray:
```sh
```sh
"log": {
"access": "./access.log",
"dnsLog": false,
"loglevel": "warning"
},
```
- **Para versiones `v2.1.3` y posteriores:**
- Hay una opción para configurar `access.log` directamente desde el panel.
```
</details>
@@ -486,7 +433,6 @@ Ingresa el ID de chat de usuario en el campo de entrada número 4. Las cuentas d
#### Uso
- [Documentación de API](https://www.postman.com/hsanaei/3x-ui/collection/q1l5l0u/3x-ui)
- `/login` con `POST` datos de usuario: `{username: '', password: ''}` para iniciar sesión
- `/panel/api/inbounds` base para las siguientes acciones:
@@ -516,7 +462,9 @@ Ingresa el ID de chat de usuario en el campo de entrada número 4. Las cuentas d
- `client.password` para TROJAN
- `client.email` para Shadowsocks
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
- [Documentación de API](https://documenter.getpostman.com/view/16802678/2s9YkgD5jm)
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415%26entityType%3Dcollection%26workspaceId%3D2cd38c01-c851-4a15-a972-f181c23359d9)
</details>
## Variables de Entorno
@@ -544,34 +492,13 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
## Vista previa
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/01-overview-dark.png">
<img alt="3x-ui" src="./media/01-overview-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/02-inbounds-dark.png">
<img alt="3x-ui" src="./media/02-inbounds-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/03-add-inbound-dark.png">
<img alt="3x-ui" src="./media/03-add-inbound-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/04-add-client-dark.png">
<img alt="3x-ui" src="./media/04-add-client-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/05-settings-dark.png">
<img alt="3x-ui" src="./media/05-settings-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/06-configs-dark.png">
<img alt="3x-ui" src="./media/06-configs-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/07-bot-dark.png">
<img alt="3x-ui" src="./media/07-bot-light.png">
</picture>
![1](./media/1.png)
![2](./media/2.png)
![3](./media/3.png)
![4](./media/4.png)
![5](./media/5.png)
![6](./media/6.png)
![7](./media/7.png)
## Un agradecimiento especial a
@@ -580,8 +507,8 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
## Reconocimientos
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (Licencia: **GPL-3.0**): _Reglas de enrutamiento mejoradas de v2ray/xray y v2ray/xray-clients con dominios iraníes integrados y un enfoque en seguridad y bloqueo de anuncios._
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (License: **GPL-3.0**): _Este repositorio contiene reglas de enrutamiento de V2Ray actualizadas automáticamente basadas en datos de dominios y direcciones bloqueados en Rusia._
- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _Un dominio alojado en Vietnam y una lista de bloqueo con la máxima eficiencia para vietnamitas._
## Estrellas a lo largo del tiempo
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg)](https://starchart.cc/MHSanaei/3x-ui)

View File

@@ -1,11 +1,6 @@
[English](/README.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/3x-ui-dark.png">
<img alt="3x-ui" src="./media/3x-ui-light.png">
</picture>
</p>
<p align="center"><a href="#"><img src="./media/3X-UI.png" alt="Image"></a></p>
**An Advanced Web Panel • Built on Xray Core**
@@ -35,12 +30,12 @@
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```
## Install legacy Version (we don't recommend)
## Install Custom Version
To install your desired version, use following installation command. e.g., ver `v1.7.9`:
To install your desired version, add the version to the end of the installation command. e.g., ver `v2.3.15`:
```
VERSION=v1.7.9 && bash <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/$VERSION/install.sh") $VERSION
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.3.15
```
## SSL Certificate
@@ -59,8 +54,6 @@ To manage SSL certificates using ACME:
- **Get SSL:** Obtain SSL certificates.
- **Revoke:** Revoke existing SSL certificates.
- **Force Renew:** Force renewal of SSL certificates.
- **Show Existing Domains:** Display all domain certificates available on the server.
- **Set Certificate Paths for the Panel:** Specify the certificate for your domain to be used by the panel.
### Certbot
@@ -176,8 +169,6 @@ systemctl restart x-ui
docker compose up -d
```
Add ```--pull always``` flag to make docker automatically recreate container if a newer image is pulled. See https://docs.docker.com/reference/cli/docker/container/run/#pull for more info.
**OR**
```sh
@@ -252,18 +243,15 @@ location /sub {
- Ubuntu 20.04+
- Debian 11+
- CentOS 8+
- OpenEuler 22.03+
- Fedora 36+
- Arch Linux
- Parch Linux
- Manjaro
- Armbian
- AlmaLinux 8.0+
- Rocky Linux 8+
- AlmaLinux 9+
- Rocky Linux 9+
- Oracle Linux 8+
- OpenSUSE Tubleweed
- Amazon Linux 2023
- Windows x64
## Supported Architectures and Devices
@@ -290,17 +278,14 @@ Our platform offers compatibility with a diverse range of architectures and devi
## Languages
- English
- Persian
- Traditional Chinese
- Simplified Chinese
- Japanese
- Farsi
- Chinese
- Russian
- Vietnamese
- Spanish
- Indonesian
- Ukrainian
- Turkish
- Português (Brazil)
## Features
@@ -326,14 +311,11 @@ Our platform offers compatibility with a diverse range of architectures and devi
<details>
<summary>Click for default settings details</summary>
### Username, Password, Port, and Web Base Path
### Username & Password & webbasepath:
If you choose not to modify these settings, they will be generated randomly (this does not apply to Docker).
These will be generated randomly if you skip modifying them.
**Default Settings for Docker:**
- **Username:** admin
- **Password:** admin
- **Port:** 2053
- **Port:** the default port for panel is `2053`
### Database Management:
@@ -376,6 +358,17 @@ If you choose not to modify these settings, they will be generated randomly (thi
WARP is built-in, and no additional installation is required. Simply turn on the necessary configuration in the panel.
**For versions before `v2.1.0`:**
1. Run the `x-ui` command in the terminal, then choose `WARP Management`.
2. You will see the following options:
- **Account Type (free, plus, team):** Choose the appropriate account type.
- **Enable/Disable WireProxy:** Toggle WireProxy on or off.
- **Uninstall WARP:** Remove the WARP application.
3. Configure the settings as needed in the panel.
</details>
## IP Limit
@@ -494,7 +487,6 @@ Enter the user ID in input field number 4. The Telegram accounts with this id wi
#### Usage
- [API Documentation](https://www.postman.com/hsanaei/3x-ui/collection/q1l5l0u/3x-ui)
- `/login` with `POST` user data: `{username: '', password: ''}` for login
- `/panel/api/inbounds` base for following actions:
@@ -525,7 +517,9 @@ Enter the user ID in input field number 4. The Telegram accounts with this id wi
- `client.password` for TROJAN
- `client.email` for Shadowsocks
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
- [API Documentation](https://documenter.getpostman.com/view/16802678/2s9YkgD5jm)
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415%26entityType%3Dcollection%26workspaceId%3D2cd38c01-c851-4a15-a972-f181c23359d9)
</details>
## Environment Variables
@@ -553,34 +547,13 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
## Preview
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/01-overview-dark.png">
<img alt="3x-ui" src="./media/01-overview-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/02-inbounds-dark.png">
<img alt="3x-ui" src="./media/02-inbounds-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/03-add-inbound-dark.png">
<img alt="3x-ui" src="./media/03-add-inbound-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/04-add-client-dark.png">
<img alt="3x-ui" src="./media/04-add-client-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/05-settings-dark.png">
<img alt="3x-ui" src="./media/05-settings-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/06-configs-dark.png">
<img alt="3x-ui" src="./media/06-configs-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/07-bot-dark.png">
<img alt="3x-ui" src="./media/07-bot-light.png">
</picture>
![1](./media/1.png)
![2](./media/2.png)
![3](./media/3.png)
![4](./media/4.png)
![5](./media/5.png)
![6](./media/6.png)
![7](./media/7.png)
## A Special Thanks to
@@ -589,8 +562,8 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
## Acknowledgment
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (License: **GPL-3.0**): _This repository contains automatically updated V2Ray routing rules based on data on blocked domains and addresses in Russia._
- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _A hosted domain hosted in Vietnam and blocklist with the most efficiency for Vietnamese._
## Stargazers over Time
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg)](https://starchart.cc/MHSanaei/3x-ui)

View File

@@ -1,11 +1,6 @@
[English](/README.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/3x-ui-dark.png">
<img alt="3x-ui" src="./media/3x-ui-light.png">
</picture>
</p>
<p align="center"><a href="#"><img src="./media/3X-UI.png" alt="Image"></a></p>
**Продвинутая веб-панель • Построена на основе Xray Core**
@@ -17,7 +12,7 @@
> **Отказ от ответственности:** Этот проект предназначен только для личного обучения и общения. Пожалуйста, не используйте его в незаконных целях и не применяйте в производственной среде.
**Если этот проект оказался полезным для вас, вы можете оценить его, поставив звёздочку** :star2:
**Если этот проект оказался полезным для вас, вы можете оценить его, постативив звёздочку** :star2:
<p align="left">
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
@@ -35,12 +30,12 @@
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```
## Установить старую версию (мы не рекомендуем)
## Установка определённой версии
Чтобы установить желаемую версию, используйте следующую команду установки. Например, ver `v1.7.9`:
Чтобы установить нужную вам версию, добавьте номер версии в конец команды установки. Например, `v2.3.15`:
```
VERSION=v1.7.9 && <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/$VERSION/install.sh") $VERSION
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.3.15
```
## SSL Сертификат
@@ -58,9 +53,7 @@ VERSION=v1.7.9 && <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/$
- **Get SSL:** Получить SSL сертификаты.
- **Revoke:** Отозвать существующие SSL сертификаты.
- **Force Renew:** Принудительно перевыпустить SSL сертификаты.
- **Show Existing Domains:** Отобразить все сертификаты доменов, доступные на сервере.
- **Set Certificate Paths for the Panel:** Укажите сертификат для вашего домена, который будет использоваться панелью.
- **Force Renew:** Принудительно превыпустить SSL сертификаты.
### Certbot
@@ -83,7 +76,7 @@ certbot renew --dry-run
**Как получить глобальный API-ключ Cloudflare:**
1. Выполните команду `x-ui` в терминале, затем выберите `Cloudflare SSL Certificate`.
2. Перейдите по ссылке: [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens).
2. Посетите ссылку: [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens).
3. Нажмите на "View Global API Key" (см. скриншот ниже):
![](media/APIKey1.PNG)
4. Возможно, вам потребуется повторно пройти аутентификацию. После этого ключ API будет отображён (см. скриншот ниже):
@@ -175,8 +168,6 @@ systemctl restart x-ui
docker compose up -d
```
Добавьте параметр ```--pull always``` для автоматического обновления контейнера, когда публикуется новый образ. Подробности: https://docs.docker.com/reference/cli/docker/container/run/#pull
**ИЛИ**
```sh
@@ -251,18 +242,15 @@ location /sub {
- Ubuntu 20.04+
- Debian 11+
- CentOS 8+
- OpenEuler 22.03+
- Fedora 36+
- Arch Linux
- Parch Linux
- Manjaro
- Armbian
- AlmaLinux 8.0+
- Rocky Linux 8+
- AlmaLinux 9+
- Rocky Linux 9+
- Oracle Linux 8+
- OpenSUSE Tubleweed
- Amazon Linux 2023
- Windows x64
## Поддерживаемые архитектуры и устройства
@@ -288,18 +276,15 @@ location /sub {
## Языки
- English (английский)
- Persian (персидский)
- Traditional Chinese (традиционный китайский)
- Simplified Chinese (упрощенный китайский)
- Japanese (японский)
- Russian (русский)
- Vietnamese (вьетнамский)
- Spanish (испанский)
- Indonesian (индонезийский)
- Ukrainian (украинский)
- Turkish (турецкий)
- Português (Brazil) (португальский (Бразилия))
- Английский
- Фарси
- Китайский
- Русский
- Вьетнамский
- Испанский
- Индонезийский
- Украинский
- Турецкий
## Возможности
@@ -323,14 +308,11 @@ location /sub {
<details>
<summary>Нажмите для получения информации о настройках по умолчанию</summary>
### Имя пользователя, Пароль, Порт и Web Base Path
### Имя пользователя и пароль & webbasepath:
Если вы не измените эти настройки, они будут сгенерированы случайным образом (это не относится к Docker).
Эти параметры будут сгенерированы случайным образом, если вы пропустите их изменение.
**Настройки по умолчанию для Docker:**
- **Имя пользователя:** admin
- **Пароль:** admin
- **Порт:** 2053
- **Порт:** порт панели по умолчанию — `2053`
### Управление базой данных:
@@ -347,7 +329,7 @@ location /sub {
- Выберите опцию `Reset Web Base Path`.
2. **Генерация или настройка пути:**
- Путь будет сгенерирован случайным образом, или вы можете ввести собственный путь.
- Путь будет случайным образом сгенерирован, или вы можете ввести пользовательский путь.
3. **Просмотр текущих настроек:**
- Чтобы просмотреть текущие настройки, используйте команду `x-ui settings` в терминале или опцию `View Current Settings` в `x-ui`.
@@ -372,6 +354,17 @@ location /sub {
WARP встроен, и дополнительная установка не требуется. Просто включите необходимую конфигурацию в панели.
**Для версий до `v2.1.0`:**
1. Выполните команду `x-ui` в терминале, затем выберите `WARP Management`.
2. Вам будут предложены следующие опции:
- **Account Type (free, plus, team):** Выбрать соответствующий тип учетной записи.
- **Enable/Disable WireProxy:** Включить или отключить WireProxy.
- **Uninstall WARP:** Удалить приложение WARP.
3. Настройте параметры по мере необходимости в панели.
</details>
## Ограничение IP
@@ -448,7 +441,7 @@ WARP встроен, и дополнительная установка не т
- Периодические отчеты
- Уведомления о входе
- Уведомления о пороге загруженности процессора
- Уведомления о пороге CPU
- Уведомления о времени истечения и трафике заранее
- Поддерживает меню отчетов клиента, если имя пользователя телеграм клиента добавлено в конфигурации пользователя
- Поддержка отчета о трафике через Telegram, поиск по UUID (VMESS/VLESS) или паролю (TROJAN) - анонимно
@@ -462,7 +455,7 @@ WARP встроен, и дополнительная установка не т
### Настройка телеграм-бота
- Запустите [Botfather](https://t.me/BotFather) в вашем аккаунте Telegram:
- Запустить [Botfather](https://t.me/BotFather) в вашем аккаунте Telegram:
![Botfather](./media/botfather.png)
- Создайте нового бота с помощью команды /newbot: у вас спросят 2 вопроса: отображаемое имя и имя пользователя для вашего бота. Обратите внимание, что имя пользователя должно заканчиваться на слово "bot".
@@ -477,7 +470,7 @@ WARP встроен, и дополнительная установка не т
Введите токен вашего бота в поле ввода номер 3.
Введите ID пользователя в поле ввода номер 4. Telegram-аккаунты с этим ID будут администраторами бота. (Вы можете ввести несколько ID, разделяя их запятой)
- Как получить ID пользователя Telegram? Используйте этот [бот](https://t.me/useridinfobot). Запустите бота, и он отобразит ваш ID пользователя Telegram.
- Как получить ID пользователя Telegram? Используйте этого [бота](https://t.me/useridinfobot). Запустите бота, и он предоставит вам ваше ID пользователя Telegram.
![ID пользователя](./media/user-id.png)
</details>
@@ -489,7 +482,6 @@ WARP встроен, и дополнительная установка не т
#### Использование
- [API документация](https://www.postman.com/hsanaei/3x-ui/collection/q1l5l0u/3x-ui)
- `/login` с `POST`-данными: `{username: '', password: ''}` для входа
- `/panel/api/inbounds` это базовый путь для следующих действий:
@@ -512,7 +504,7 @@ WARP встроен, и дополнительная установка не т
| `POST` | `"/resetAllTraffics"` | Сбросить трафик всех входящих соединений
| `POST` | `"/resetAllClientTraffics/:id"` | Сбросить трафик всех клиентов в входящем соединении
| `POST` | `"/delDepletedClients/:id"` | Удалить истекших клиентов в входящем соединении (-1: всех)
| `POST` | `"/onlines"` | Получить пользователей, которые находятся онлайн (список email'ов)
| `POST` | `"/onlines"` | Получить пользователей, которые онлайн (список email'ов)
\*- Поле `clientId` должно быть заполнено следующим образом:
@@ -523,7 +515,8 @@ WARP встроен, и дополнительная установка не т
</details>
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
- [API-документация](https://documenter.getpostman.com/view/16802678/2s9YkgD5jm)
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415%26entityType%3Dcollection%26workspaceId%3D2cd38c01-c851-4a15-a972-f181c23359d9)
</details>
## Переменные среды
@@ -551,34 +544,13 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
## Предварительный Просмотр
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/01-overview-dark.png">
<img alt="3x-ui" src="./media/01-overview-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/02-inbounds-dark.png">
<img alt="3x-ui" src="./media/02-inbounds-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/03-add-inbound-dark.png">
<img alt="3x-ui" src="./media/03-add-inbound-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/04-add-client-dark.png">
<img alt="3x-ui" src="./media/04-add-client-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/05-settings-dark.png">
<img alt="3x-ui" src="./media/05-settings-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/06-configs-dark.png">
<img alt="3x-ui" src="./media/06-configs-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/07-bot-dark.png">
<img alt="3x-ui" src="./media/07-bot-light.png">
</picture>
![1](./media/1.png)
![2](./media/2.png)
![3](./media/3.png)
![4](./media/4.png)
![5](./media/5.png)
![6](./media/6.png)
![7](./media/7.png)
## Особая благодарность
@@ -587,8 +559,8 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
## Благодарности
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (License: **GPL-3.0**): _Этот репозиторий содержит автоматически обновляемые правила маршрутизации V2Ray на основе данных о заблокированных доменах и адресах в России._
- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _A hosted domain hosted in Vietnam and blocklist with the most efficiency for Vietnamese._
## Число звёзд со временем
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg)](https://starchart.cc/MHSanaei/3x-ui)

View File

@@ -1,11 +1,6 @@
[English](/README.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/3x-ui-dark.png">
<img alt="3x-ui" src="./media/3x-ui-light.png">
</picture>
</p>
<p align="center"><a href="#"><img src="./media/3X-UI.png" alt="Image"></a></p>
**一个更好的面板 • 基于Xray Core构建**
@@ -35,62 +30,38 @@
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```
## 安装旧版本 (我们不建议)
## 安装指定版本
要安装您想要的版本,请使用以下安装命令。例如,ver `v1.7.9`:
要安装所需的版本,请将该版本添加到安装命令的末尾。 e.g., ver `v2.3.15`:
```
VERSION=v1.7.9 && <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/$VERSION/install.sh") $VERSION
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.3.15
```
### SSL证
## SSL
<details>
<summary>点击查看SSL证书详情</summary>
<summary>点击查看 SSL证</summary>
### ACME
### Cloudflare
使用ACME管理SSL证书
管理脚本具有用于 Cloudflare 的内置 SSL 证书应用程序。若要使用此脚本申请证书,需要满足以下条件
1. 确保您的域名正确解析到服务器。
2. 在终端中运行 `x-ui` 命令,然后选择 `SSL证书管理`
3. 您将看到以下选项:
- Cloudflare 邮箱地址
- Cloudflare Global API Key
- 域名已通过 cloudflare 解析到当前服务器
**1:** 在终端中运行`x-ui` 选择 `Cloudflare SSL Certificate`.
- **Get SSL:** 获取SSL证书。
- **Revoke:** 吊销现有的SSL证书。
- **Force Renew:** 强制更新SSL证书。
- **Show Existing Domains:** 显示服务器上所有可用的域证书。
- **Set Certificate Paths for the Panel:** 指定用于面板的域证书。
### Certbot
安装并使用Certbot
```sh
```
apt-get install certbot -y
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
certbot renew --dry-run
```
### Cloudflare
管理脚本内置了Cloudflare的SSL证书申请。要使用此脚本申请证书您需要以下信息
- Cloudflare注册的电子邮件
- Cloudflare全局API密钥
- 域名必须通过Cloudflare解析到当前服务器
**如何获取Cloudflare全局API密钥**
1. 在终端中运行 `x-ui` 命令,然后选择 `Cloudflare SSL证书`
2. 访问链接:[Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens)。
3. 点击“查看全局API密钥”参见下图
![](media/APIKey1.PNG)
4. 您可能需要重新验证您的账户。之后将显示API密钥参见下图
![](media/APIKey2.png)
使用时,只需输入您的 `域名``电子邮件``API密钥`。如下图所示:
![](media/DetailEnter.png)
***Tip:*** *管理脚本具有 Certbot 。使用 `x-ui` 命令, 选择 `SSL Certificate Management`.*
</details>
@@ -248,18 +219,13 @@ location /sub {
- Ubuntu 20.04+
- Debian 11+
- CentOS 8+
- OpenEuler 22.03+
- Fedora 36+
- Arch Linux
- Parch Linux
- Manjaro
- Armbian
- AlmaLinux 8.0+
- Rocky Linux 8+
- Oracle Linux 8+
- AlmaLinux 9+
- Rockylinux 9+
- OpenSUSE Tubleweed
- Amazon Linux 2023
- Windows x64
## 支持的架构和设备
<details>
@@ -282,18 +248,14 @@ location /sub {
## Languages
- English英语
- Persian波斯语
- Traditional Chinese繁体中文)
- Simplified Chinese简体中文
- Japanese日语
- Russian俄语
- Vietnamese越南语
- Spanish西班牙语
- Indonesian印尼语
- Ukrainian乌克兰语
- Turkish土耳其语
- Português (Brazil)(葡萄牙语(巴西))
- English英语
- Farsi(伊朗语)
- Chinese中文
- Russian俄语
- Vietnamese越南语
- Spanish西班牙语
- Indonesian (印度尼西亚语)
- Ukrainian乌克兰语
## Features
@@ -314,103 +276,88 @@ location /sub {
- 支持从面板导出/导入数据库
## 默认面板设置
## 默认设置
<details>
<summary>点击查看默认设置详情</summary>
<summary>点击查看 默认设置</summary>
### 用户名、密码、端口和 Web Base Path
### 信息
如果您选择不修改这些设置,它们将随机生成(不适用于 Docker
**Docker 的默认设置:**
- **用户名:** admin
- **密码:** admin
- **端口:** 2053
### 数据库管理:
您可以直接在面板中方便地进行数据库备份和还原。
- **数据库路径:**
- `/etc/x-ui/x-ui.db`
### Web 基础路径
1. **重置 Web 基础路径:**
- 打开终端。
- 运行 `x-ui` 命令。
- 选择 `重置 Web 基础路径` 选项。
2. **生成或自定义路径:**
- 路径将会随机生成,或者您可以输入自定义路径。
3. **查看当前设置:**
- 要查看当前设置,请在终端中使用 `x-ui settings` 命令,或在 `x-ui` 面板中点击 `查看当前设置`。
### 安全建议:
- 为了提高安全性建议在URL结构中使用一个长的随机词。
**示例:**
- `http://ip:port/*webbasepath*/panel`
- `http://domain:port/*webbasepath*/panel`
- **用户名 & 密码:** 当您跳过设置时,此项会随机生成。
- **数据库路径:**
- /etc/x-ui/x-ui.db
- **Xray 配置路径:**
- /usr/local/x-ui/bin/config.json
- **面板链接无SSL**
- http://ip:2053/panel
- http://domain:2053/panel
- **面板链接有SSL**
- https://domain:2053/panel
</details>
## WARP 配置
<details>
<summary>点击查看 WARP 配置详情</summary>
<summary>点击查看 WARP 配置</summary>
#### 使用方法
#### 使用
**对于 `v2.1.0` 及之后的版本:**
如果要在 v2.1.0 之前使用 WARP 路由,请按照以下步骤操作:
WARP 已内置,无需额外安装。只需在面板中开启相关配置即可。
**1.** 在 **SOCKS Proxy Mode** 模式中安装Wrap
```sh
bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh)
```
**2.** 如果您已经安装了 warp您可以使用以下命令卸载
```sh
warp u
```
**3.** 在面板中打开您需要的配置
配置:
- Block Ads
- Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP
- Fix Google 403 error
</details>
## IP 限制
<details>
<summary>点击查看 IP 限制详情</summary>
<summary>点击查看 IP 限制</summary>
#### 使用方法
#### 使用
**注意:** 使用 IP 隧道时IP 限制无法正常工作。
**注意** 使用 IP 隧道时IP 限制无法正常工作。
- **对于 `v1.6.1` 及之前的版本:**
- IP 限制功能已内置于面板中。
- 适用于最高 `v1.6.1`
**对于 `v1.7.0` 及更新的版本:**
- IP 限制 已被集成在面板中。
要启用 IP 限制功能,您需要安装 `fail2ban` 及其所需的文件,步骤如下
- 适用于 `v1.7.0` 以及更新的版本
1. 在终端中运行 `x-ui` 命令,然后选择 `IP 限制管理`。
2. 您将看到以下选项:
- 要使 IP 限制正常工作,您需要按照以下步骤安装 fail2ban 及其所需的文件:
- **更改封禁时长:** 调整封禁时长。
- **解除所有封禁:** 解除当前的所有封禁。
- **查看日志:** 查看日志
- **Fail2ban 状态:** 检查 `fail2ban` 的状态。
- **重启 Fail2ban:** 重启 `fail2ban` 服务。
- **卸载 Fail2ban:** 卸载带有配置的 Fail2ban。
1. 使用面板内置的 `x-ui` 指令
2. 选择 `IP Limit Management`.
3. 根据您的需要选择合适的选项
3. 在面板中通过设置 `Xray 配置/log/访问日志` 为 `./access.log` 添加访问日志路径,然后保存并重启 Xray
- 确保您的 Xray 配置上有 ./access.log 。在 v2.1.3 之后,我们有一个选项
- **对于 `v2.1.3` 之前的版本:**
- 您需要在 Xray 配置中手动设置访问日志路径:
```sh
```sh
"log": {
"access": "./access.log",
"dnsLog": false,
"loglevel": "warning"
},
```
- **对于 `v2.1.3` 及之后的版本:**
- 面板中直接提供了配置 `access.log` 的选项。
```
</details>
@@ -486,7 +433,6 @@ Web 面板通过 Telegram Bot 支持每日流量、面板登录、数据库备
#### 使用
- [API 文档](https://www.postman.com/hsanaei/3x-ui/collection/q1l5l0u/3x-ui)
- `/login` 使用 `POST` 用户名称 & 密码: `{username: '', password: ''}` 登录
- `/panel/api/inbounds` 以下操作的基础:
@@ -516,7 +462,9 @@ Web 面板通过 Telegram Bot 支持每日流量、面板登录、数据库备
- `client.password` TROJAN
- `client.email` Shadowsocks
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
- [API 文档](https://documenter.getpostman.com/view/16802678/2s9YkgD5jm)
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415%26entityType%3Dcollection%26workspaceId%3D2cd38c01-c851-4a15-a972-f181c23359d9)
</details>
## 环境变量
@@ -544,34 +492,13 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
## 预览
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/01-overview-dark.png">
<img alt="3x-ui" src="./media/01-overview-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/02-inbounds-dark.png">
<img alt="3x-ui" src="./media/02-inbounds-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/03-add-inbound-dark.png">
<img alt="3x-ui" src="./media/03-add-inbound-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/04-add-client-dark.png">
<img alt="3x-ui" src="./media/04-add-client-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/05-settings-dark.png">
<img alt="3x-ui" src="./media/05-settings-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/06-configs-dark.png">
<img alt="3x-ui" src="./media/06-configs-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/07-bot-dark.png">
<img alt="3x-ui" src="./media/07-bot-light.png">
</picture>
![1](./media/1.png)
![2](./media/2.png)
![3](./media/3.png)
![4](./media/4.png)
![5](./media/5.png)
![6](./media/6.png)
![7](./media/7.png)
## 特别感谢
@@ -580,8 +507,8 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
## 致谢
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (License: **GPL-3.0**): _This repository contains automatically updated V2Ray routing rules based on data on blocked domains and addresses in Russia._
- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _A hosted domain hosted in Vietnam and blocklist with the most efficiency for Vietnamese._
## Star趋势
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg)](https://starchart.cc/MHSanaei/3x-ui)

View File

@@ -1 +1 @@
2.5.1
2.3.15

View File

@@ -26,35 +26,20 @@ const (
)
func initModels() error {
// Order matters: first create tables without dependencies
baseModels := []interface{}{
models := []interface{}{
&model.User{},
&model.Setting{},
}
// Migrate base models
for _, model := range baseModels {
if err := db.AutoMigrate(model); err != nil {
log.Printf("Error auto migrating base model: %v", err)
return err
}
}
// Then migrate models with dependencies
dependentModels := []interface{}{
&model.Inbound{},
&model.OutboundTraffics{},
&model.Setting{},
&model.InboundClientIps{},
&xray.ClientTraffic{},
}
for _, model := range dependentModels {
for _, model := range models {
if err := db.AutoMigrate(model); err != nil {
log.Printf("Error auto migrating dependent model: %v", err)
log.Printf("Error auto migrating model: %v", err)
return err
}
}
return nil
}
@@ -97,31 +82,9 @@ func InitDB(dbPath string) error {
}
c := &gorm.Config{
Logger: gormLogger,
SkipDefaultTransaction: true,
PrepareStmt: true,
Logger: gormLogger,
}
dsn := dbPath + "?cache=shared&_journal_mode=WAL&_synchronous=NORMAL"
db, err = gorm.Open(sqlite.Open(dsn), c)
if err != nil {
return err
}
sqlDB, err := db.DB()
if err != nil {
return err
}
_, err = sqlDB.Exec("PRAGMA cache_size = -64000;")
if err != nil {
return err
}
_, err = sqlDB.Exec("PRAGMA temp_store = MEMORY;")
if err != nil {
return err
}
_, err = sqlDB.Exec("PRAGMA foreign_keys = ON;")
db, err = gorm.Open(sqlite.Open(dbPath), c)
if err != nil {
return err
}
@@ -138,11 +101,6 @@ func InitDB(dbPath string) error {
func CloseDB() error {
if db != nil {
if err := Checkpoint(); err != nil {
log.Printf("error executing checkpoint: %v", err)
}
sqlDB, err := db.DB()
if err != nil {
return err

View File

@@ -29,14 +29,14 @@ type User struct {
type Inbound struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
UserId int `json:"-" gorm:"index"`
UserId int `json:"-"`
Up int64 `json:"up" form:"up"`
Down int64 `json:"down" form:"down"`
Total int64 `json:"total" form:"total"`
Remark string `json:"remark" form:"remark"`
Enable bool `json:"enable" form:"enable"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id;constraint:OnDelete:CASCADE" json:"clientStats"`
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"`
// config part
Listen string `json:"listen" form:"listen"`
@@ -46,7 +46,6 @@ type Inbound struct {
StreamSettings string `json:"streamSettings" form:"streamSettings"`
Tag string `json:"tag" form:"tag" gorm:"unique"`
Sniffing string `json:"sniffing" form:"sniffing"`
Allocate string `json:"allocate" form:"allocate"`
}
type OutboundTraffics struct {
@@ -76,7 +75,6 @@ func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
StreamSettings: json_util.RawMessage(i.StreamSettings),
Tag: i.Tag,
Sniffing: json_util.RawMessage(i.Sniffing),
Allocate: json_util.RawMessage(i.Allocate),
}
}
@@ -98,6 +96,5 @@ type Client struct {
Enable bool `json:"enable" form:"enable"`
TgID int64 `json:"tgId" form:"tgId"`
SubID string `json:"subId" form:"subId"`
Comment string `json:"comment" form:"comment"`
Reset int `json:"reset" form:"reset"`
}

View File

@@ -11,7 +11,6 @@ services:
- $PWD/cert/:/root/cert/
environment:
XRAY_VMESS_AEAD_FORCED: "false"
X_UI_ENABLE_FAIL2BAN: "true"
tty: true
network_mode: host
restart: unless-stopped

105
go.mod
View File

@@ -1,45 +1,46 @@
module x-ui
go 1.23.5
go 1.23.0
require (
github.com/gin-contrib/gzip v1.2.2
github.com/gin-contrib/sessions v1.0.2
github.com/gin-contrib/gzip v1.0.1
github.com/gin-contrib/sessions v1.0.1
github.com/gin-gonic/gin v1.10.0
github.com/goccy/go-json v0.10.5
github.com/mymmrac/telego v0.32.0
github.com/nicksnyder/go-i18n/v2 v2.5.1
github.com/goccy/go-json v0.10.3
github.com/mymmrac/telego v0.31.2
github.com/nicksnyder/go-i18n/v2 v2.4.0
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.2.3
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.25.1
github.com/valyala/fasthttp v1.58.0
github.com/xtls/xray-core v1.8.25-0.20250130105737-0a8470cb14eb
github.com/shirou/gopsutil/v4 v4.24.8
github.com/valyala/fasthttp v1.55.0
github.com/xtls/xray-core v1.8.24
go.uber.org/atomic v1.11.0
golang.org/x/text v0.21.0
google.golang.org/grpc v1.70.0
gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.25.12
golang.org/x/text v0.18.0
google.golang.org/grpc v1.66.0
gorm.io/driver/sqlite v1.5.6
gorm.io/gorm v1.25.11
)
require (
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/bytedance/sonic v1.12.8 // indirect
github.com/bytedance/sonic/loader v0.2.3 // indirect
github.com/cloudflare/circl v1.5.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/fasthttp/router v1.5.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/bytedance/sonic v1.12.2 // indirect
github.com/bytedance/sonic/loader v0.2.0 // indirect
github.com/cloudflare/circl v1.4.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
github.com/fasthttp/router v1.5.2 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
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.24.0 // indirect
github.com/go-playground/validator/v10 v10.22.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/pprof v0.0.0-20250202011525-fc3143867406 // indirect
github.com/google/pprof v0.0.0-20240903155634-a8630aee4ab9 // indirect
github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.4.0 // indirect
@@ -48,54 +49,54 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
github.com/lufia/plan9stats v0.0.0-20240819163618-b1d8f4d146e7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.24 // indirect
github.com/mattn/go-sqlite3 v1.14.23 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/pires/go-proxyproto v0.8.0 // indirect
github.com/onsi/ginkgo/v2 v2.20.2 // indirect
github.com/pires/go-proxyproto v0.7.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.49.0 // indirect
github.com/quic-go/qpack v0.5.0 // indirect
github.com/quic-go/quic-go v0.46.0 // indirect
github.com/refraction-networking/utls v1.6.7 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/sagernet/sing v0.5.1 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/sagernet/sing v0.4.2 // indirect
github.com/sagernet/sing-shadowsocks v0.2.7 // indirect
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.9.0 // indirect
github.com/tklauser/numcpus v0.8.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
github.com/vishvananda/netlink v1.3.0 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/xtls/reality v0.0.0-20240712055506-48f0b2d5ed6d // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/mock v0.5.0 // indirect
go.uber.org/mock v0.4.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.13.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.29.0 // indirect
golang.org/x/arch v0.10.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/time v0.6.0 // indirect
golang.org/x/tools v0.24.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 // indirect
google.golang.org/protobuf v1.36.4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20240320123526-dc6abceb7ff0 // indirect
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 // indirect
lukechampine.com/blake3 v1.3.0 // indirect
)

401
go.sum
View File

@@ -1,46 +1,66 @@
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0 h1:Wo41lDOevRJSGpevP+8Pk5bANX7fJacO2w04aqLiC5I=
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0/go.mod h1:FVGavL/QEBQDcBpr3fAojoK17xX5k9bicBphrOpP7uM=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs=
github.com/bytedance/sonic v1.12.8/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg=
github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY=
github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM=
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fasthttp/router v1.5.4 h1:oxdThbBwQgsDIYZ3wR1IavsNl6ZS9WdjKukeMikOnC8=
github.com/fasthttp/router v1.5.4/go.mod h1:3/hysWq6cky7dTfzaaEPZGdptwjwx0qzTgFCKEWRjgc=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fasthttp/router v1.5.2 h1:ckJCCdV7hWkkrMeId3WfEhz+4Gyyf6QPwxi/RHIMZ6I=
github.com/fasthttp/router v1.5.2/go.mod h1:C8EY53ozOwpONyevc/V7Gr8pqnEjwnkFFqPo1alAGs0=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/gin-contrib/gzip v1.2.2 h1:iUU/EYCM8ENfkjmZaVrxbjF/ZC267Iqv5S0MMCMEliI=
github.com/gin-contrib/gzip v1.2.2/go.mod h1:C1a5cacjlDsS20cKnHlZRCPUu57D3qH6B2pV0rl+Y/s=
github.com/gin-contrib/sessions v1.0.2 h1:UaIjUvTH1cMeOdj3in6dl+Xb6It8RiKRF9Z1anbUyCA=
github.com/gin-contrib/sessions v1.0.2/go.mod h1:KxKxWqWP5LJVDCInulOl4WbLzK2KSPlLesfZ66wRvMs=
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE=
github.com/gin-contrib/gzip v1.0.1/go.mod h1:njt428fdUNRvjuJf16tZMYZ2Yl+WQB53X5wmhDwXvC4=
github.com/gin-contrib/sessions v1.0.1 h1:3hsJyNs7v7N8OtelFmYXFrulAf6zSR7nW/putcPEHxI=
github.com/gin-contrib/sessions v1.0.1/go.mod h1:ouxSFM24/OgIud5MJYQJLpy6AwxQ5EYO9yLhbtObGkM=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@@ -50,27 +70,39 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
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=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250202011525-fc3143867406 h1:wlQI2cYY0BsWmmPPAnxfQ8SDW0S3Jasn+4B8kXFxprg=
github.com/google/pprof v0.0.0-20250202011525-fc3143867406/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20240903155634-a8630aee4ab9 h1:q5g0N9eal4bmJwXHC5z0QCKs8qhS35hFfq0BAYsIwZI=
github.com/google/pprof v0.0.0-20240903155634-a8630aee4ab9/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
@@ -81,96 +113,148 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc=
github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/lufia/plan9stats v0.0.0-20240819163618-b1d8f4d146e7 h1:5RK988zAqB3/AN3opGfRpoQgAVqr6/A5+qRTi67VUZY=
github.com/lufia/plan9stats v0.0.0-20240819163618-b1d8f4d146e7/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
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.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
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 v0.32.0 h1:4X8C1l3k+opkk86r95+eQE8DxiS2LYlR61L/G7yreDY=
github.com/mymmrac/telego v0.32.0/go.mod h1:qS6NaRhJgcuEEBEMVCV79S2xCAuHq9O+ixwfLuRW31M=
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/mymmrac/telego v0.31.2 h1:srvQOQtb5ZswmqIr03VuAkIF076bi25n7fyQ51Ifstw=
github.com/mymmrac/telego v0.31.2/go.mod h1:dyuyrOIagRstnm2ZNWuVilPdsslQyEgwYww9zkDqdJU=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4=
github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pires/go-proxyproto v0.8.0 h1:5unRmEAPbHXHuLjDg01CxJWf91cw3lKHc/0xzKpXEe0=
github.com/pires/go-proxyproto v0.8.0/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.49.0 h1:w5iJHXwHxs1QxyBv1EHKuC50GX5to8mJAxvtnttJp94=
github.com/quic-go/quic-go v0.49.0/go.mod h1:s2wDnmCdooUQBmQfpUSTCYBl1/D4FcqbULMMkASvR6s=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/quic-go/qpack v0.5.0 h1:jldbr38Ef/swDfxtvNvvUIYNg5LNm3Oa9W+IZvCm4q0=
github.com/quic-go/qpack v0.5.0/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.46.0 h1:uuwLClEEyk1DNvchH8uCByQVjo3yKL9opKulExNDs7Y=
github.com/quic-go/quic-go v0.46.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI=
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
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=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sagernet/sing v0.4.2 h1:jzGNJdZVRI0xlAfFugsIQUPvyB9SuWvbJK7zQCXc4QM=
github.com/sagernet/sing v0.4.2/go.mod h1:ieZHA/+Y9YZfXs2I3WtuwgyCZ6GPsIR7HdKb1SdEnls=
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shirou/gopsutil/v4 v4.24.8 h1:pVQjIenQkIhqO81mwTaXjTzOMT7d3TZkf43PlVFHENI=
github.com/shirou/gopsutil/v4 v4.24.8/go.mod h1:wE0OrJtj4dG+hYkxqDH3QiBICdKSf04/npcvLLc/oRg=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
@@ -179,90 +263,145 @@ github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE=
github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw=
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463 h1:g1Cj7d+my6k/HHxLAyxPwyX8i7FGRr6ulBDMkBzg2BM=
github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463/go.mod h1:BjIOLmkEEtAgloAiVUcYj0Mt+YU00JARZw8AEU0IwAg=
github.com/xtls/xray-core v1.8.25-0.20250130105737-0a8470cb14eb h1:VBzDZ4XHT9FM0S3qvXp6KuTOk7mXQQm0pjOW0Neeog4=
github.com/xtls/xray-core v1.8.25-0.20250130105737-0a8470cb14eb/go.mod h1:EKhNEDY/WIB+ylEbVv2pzUaP08W/lt0sYmJQ9FJruko=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/xtls/reality v0.0.0-20240712055506-48f0b2d5ed6d h1:+B97uD9uHLgAAulhigmys4BVwZZypzK7gPN3WtpgRJg=
github.com/xtls/reality v0.0.0-20240712055506-48f0b2d5ed6d/go.mod h1:dm4y/1QwzjGaK17ofi0Vs6NpKAHegZky8qk6J2JJZAE=
github.com/xtls/xray-core v1.8.24 h1:Y2NumdlnJ9C9gvh1Ivs2+73ui5XQgB70wZXYCiI9DyY=
github.com/xtls/xray-core v1.8.24/go.mod h1:cWIOI6iBBOsB0HHU9PGhaiBhaMPfiktUjwA0IWolWJc=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8=
golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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.5.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.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
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-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 h1:J1H9f+LEdWAfHcez/4cvaVBox7cOYT+IU6rgqj5x++8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c=
google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gvisor.dev/gvisor v0.0.0-20240320123526-dc6abceb7ff0 h1:P+U/06iIKPQ3DLcg+zBfSCia1luZ2msPZrJ8jYDFPs0=
gvisor.dev/gvisor v0.0.0-20240320123526-dc6abceb7ff0/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0=
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 h1:ze1vwAdliUAr68RQ5NtufWaXaOg8WUO2OACzEV+TNdE=
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=

View File

@@ -2,7 +2,6 @@
red='\033[0;31m'
green='\033[0;32m'
blue='\033[0;34m'
yellow='\033[0;33m'
plain='\033[0m'
@@ -40,53 +39,43 @@ arch() {
echo "arch: $(arch)"
os_version=""
os_version=$(grep "^VERSION_ID" /etc/os-release | cut -d '=' -f2 | tr -d '"' | tr -d '.')
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
if [[ "${release}" == "arch" ]]; then
echo "Your OS is Arch Linux"
elif [[ "${release}" == "parch" ]]; then
echo "Your OS is Parch Linux"
echo "Your OS is Parch linux"
elif [[ "${release}" == "manjaro" ]]; then
echo "Your OS is Manjaro"
elif [[ "${release}" == "armbian" ]]; then
echo "Your OS is Armbian"
elif [[ "${release}" == "alpine" ]]; then
echo "Your OS is Alpine Linux"
elif [[ "${release}" == "opensuse-tumbleweed" ]]; then
echo "Your OS is OpenSUSE Tumbleweed"
elif [[ "${release}" == "openEuler" ]]; then
if [[ ${os_version} -lt 2203 ]]; then
echo -e "${red} Please use OpenEuler 22.03 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "centos" ]]; then
if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 2004 ]]; then
if [[ ${os_version} -lt 20 ]]; then
echo -e "${red} Please use Ubuntu 20 or higher version!${plain}\n" && exit 1
fi
elif [[ "${release}" == "fedora" ]]; then
if [[ ${os_version} -lt 36 ]]; then
echo -e "${red} Please use Fedora 36 or higher version!${plain}\n" && exit 1
fi
elif [[ "${release}" == "amzn" ]]; then
if [[ ${os_version} != "2023" ]]; then
echo -e "${red} Please use Amazon Linux 2023!${plain}\n" && exit 1
fi
elif [[ "${release}" == "debian" ]]; then
if [[ ${os_version} -lt 11 ]]; then
echo -e "${red} Please use Debian 11 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "almalinux" ]]; then
if [[ ${os_version} -lt 80 ]]; then
echo -e "${red} Please use AlmaLinux 8.0 or higher ${plain}\n" && exit 1
if [[ ${os_version} -lt 9 ]]; then
echo -e "${red} Please use AlmaLinux 9 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "rocky" ]]; then
if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use Rocky Linux 8 or higher ${plain}\n" && exit 1
if [[ ${os_version} -lt 9 ]]; then
echo -e "${red} Please use Rocky Linux 9 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "ol" ]]; then
elif [[ "${release}" == "oracle" ]]; then
if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use Oracle Linux 8 or higher ${plain}\n" && exit 1
fi
@@ -96,18 +85,17 @@ else
echo "- Ubuntu 20.04+"
echo "- Debian 11+"
echo "- CentOS 8+"
echo "- OpenEuler 22.03+"
echo "- Fedora 36+"
echo "- Arch Linux"
echo "- Parch Linux"
echo "- Manjaro"
echo "- Armbian"
echo "- AlmaLinux 8.0+"
echo "- Rocky Linux 8+"
echo "- AlmaLinux 9+"
echo "- Rocky Linux 9+"
echo "- Oracle Linux 8+"
echo "- OpenSUSE Tumbleweed"
echo "- Amazon Linux 2023"
exit 1
fi
install_base() {
@@ -115,10 +103,10 @@ install_base() {
ubuntu | debian | armbian)
apt-get update && apt-get install -y -q wget curl tar tzdata
;;
centos | almalinux | rocky | ol)
centos | almalinux | rocky | oracle)
yum -y update && yum install -y -q wget curl tar tzdata
;;
fedora | amzn)
fedora)
dnf -y update && dnf install -y -q wget curl tar tzdata
;;
arch | manjaro | parch)
@@ -139,63 +127,44 @@ gen_random_string() {
echo "$random_string"
}
# This function will be called when user installed x-ui out of security
config_after_install() {
local existing_username=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'username: .+' | awk '{print $2}')
local existing_password=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'password: .+' | awk '{print $2}')
local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')
local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
local server_ip=$(curl -s https://api.ipify.org)
if [[ ${#existing_webBasePath} -lt 4 ]]; then
if [[ "$existing_username" == "admin" && "$existing_password" == "admin" ]]; then
local config_webBasePath=$(gen_random_string 15)
local config_username=$(gen_random_string 10)
local config_password=$(gen_random_string 10)
read -p "Would you like to customize the Panel Port settings? (If not, a random port will be applied) [y/n]: " config_confirm
if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
read -p "Please set up the panel port: " config_port
echo -e "${yellow}Your Panel Port is: ${config_port}${plain}"
else
local config_port=$(shuf -i 1024-62000 -n 1)
echo -e "${yellow}Generated random port: ${config_port}${plain}"
fi
/usr/local/x-ui/x-ui setting -username "${config_username}" -password "${config_password}" -port "${config_port}" -webBasePath "${config_webBasePath}"
echo -e "This is a fresh installation, generating random login info for security concerns:"
echo -e "###############################################"
echo -e "${green}Username: ${config_username}${plain}"
echo -e "${green}Password: ${config_password}${plain}"
echo -e "${green}Port: ${config_port}${plain}"
echo -e "${green}WebBasePath: ${config_webBasePath}${plain}"
echo -e "${green}Access URL: http://${server_ip}:${config_port}/${config_webBasePath}${plain}"
echo -e "###############################################"
echo -e "${yellow}If you forgot your login info, you can type 'x-ui settings' to check${plain}"
else
local config_webBasePath=$(gen_random_string 15)
echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}"
/usr/local/x-ui/x-ui setting -webBasePath "${config_webBasePath}"
echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}"
echo -e "${green}Access URL: http://${server_ip}:${existing_port}/${config_webBasePath}${plain}"
fi
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
read -p "Would you like to customize the panel settings? (If not, random settings will be applied) [y/n]: " config_confirm
if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
read -p "Please set up your username: " config_account
echo -e "${yellow}Your username will be: ${config_account}${plain}"
read -p "Please set up your password: " config_password
echo -e "${yellow}Your password will be: ${config_password}${plain}"
read -p "Please set up the panel port: " config_port
echo -e "${yellow}Your panel port is: ${config_port}${plain}"
read -p "Please set up the web base path (ip:port/webbasepath/): " config_webBasePath
echo -e "${yellow}Your web base path is: ${config_webBasePath}${plain}"
echo -e "${yellow}Initializing, please wait...${plain}"
/usr/local/x-ui/x-ui setting -username ${config_account} -password ${config_password}
echo -e "${yellow}Account name and password set successfully!${plain}"
/usr/local/x-ui/x-ui setting -port ${config_port}
echo -e "${yellow}Panel port set successfully!${plain}"
/usr/local/x-ui/x-ui setting -webBasePath ${config_webBasePath}
echo -e "${yellow}Web base path set successfully!${plain}"
else
if [[ "$existing_username" == "admin" && "$existing_password" == "admin" ]]; then
local config_username=$(gen_random_string 10)
local config_password=$(gen_random_string 10)
echo -e "${yellow}Default credentials detected. Security update required...${plain}"
/usr/local/x-ui/x-ui setting -username "${config_username}" -password "${config_password}"
echo -e "Generated new random login credentials:"
echo -e "${red}Cancel...${plain}"
if [[ ! -f "/etc/x-ui/x-ui.db" ]]; then
local usernameTemp=$(head -c 6 /dev/urandom | base64)
local passwordTemp=$(head -c 6 /dev/urandom | base64)
local webBasePathTemp=$(gen_random_string 10)
/usr/local/x-ui/x-ui setting -username ${usernameTemp} -password ${passwordTemp} -webBasePath ${webBasePathTemp}
echo -e "This is a fresh installation, will generate random login info for security concerns:"
echo -e "###############################################"
echo -e "${green}Username: ${config_username}${plain}"
echo -e "${green}Password: ${config_password}${plain}"
echo -e "${green}Username: ${usernameTemp}${plain}"
echo -e "${green}Password: ${passwordTemp}${plain}"
echo -e "${green}WebBasePath: ${webBasePathTemp}${plain}"
echo -e "###############################################"
echo -e "${yellow}If you forgot your login info, you can type 'x-ui settings' to check${plain}"
echo -e "${yellow}If you forgot your login info, you can type "x-ui settings" to check after installation${plain}"
else
echo -e "${green}Username, Password, and WebBasePath are properly set. Exiting...${plain}"
echo -e "${yellow}This is your upgrade, will keep old settings. If you forgot your login info, you can type "x-ui settings" to check${plain}"
fi
fi
/usr/local/x-ui/x-ui migrate
}
@@ -203,32 +172,24 @@ install_x-ui() {
cd /usr/local/
if [ $# == 0 ]; then
tag_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ ! -n "$tag_version" ]]; then
echo -e "${red}Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later${plain}"
last_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ ! -n "$last_version" ]]; then
echo -e "${red}Failed to fetch x-ui version, it maybe due to Github API restrictions, please try it later${plain}"
exit 1
fi
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz
echo -e "Got x-ui latest version: ${last_version}, beginning the installation..."
wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-$(arch).tar.gz
if [[ $? -ne 0 ]]; then
echo -e "${red}Downloading x-ui failed, please be sure that your server can access GitHub ${plain}"
echo -e "${red}Downloading x-ui failed, please be sure that your server can access Github ${plain}"
exit 1
fi
else
tag_version=$1
tag_version_numeric=${tag_version#v}
min_version="2.3.5"
if [[ "$(printf '%s\n' "$min_version" "$tag_version_numeric" | sort -V | head -n1)" != "$min_version" ]]; then
echo -e "${red}Please use a newer version (at least v2.3.5). Exiting installation.${plain}"
exit 1
fi
url="https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz"
last_version=$1
url="https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-$(arch).tar.gz"
echo -e "Beginning to install x-ui $1"
wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch).tar.gz ${url}
if [[ $? -ne 0 ]]; then
echo -e "${red}Download x-ui $1 failed, please check if the version exists ${plain}"
echo -e "${red}Download x-ui $1 failed,please check the version exists ${plain}"
exit 1
fi
fi
@@ -259,28 +220,28 @@ install_x-ui() {
systemctl daemon-reload
systemctl enable x-ui
systemctl start x-ui
echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..."
echo -e "${green}x-ui ${last_version}${plain} installation finished, it is running now..."
echo -e ""
echo -e "┌───────────────────────────────────────────────────────┐
${blue}x-ui control menu usages (subcommands):${plain}
${blue}x-ui${plain} - Admin Management Script
${blue}x-ui start${plain} - Start │
${blue}x-ui stop${plain} - Stop │
${blue}x-ui restart${plain} - Restart │
${blue}x-ui status${plain} - Current Status
${blue}x-ui settings${plain} - Current Settings
${blue}x-ui enable${plain} - Enable Autostart on OS Startup
${blue}x-ui disable${plain} - Disable Autostart on OS Startup
${blue}x-ui log${plain} - Check logs │
${blue}x-ui banlog${plain} - Check Fail2ban ban logs
${blue}x-ui update${plain} - Update │
${blue}x-ui legacy${plain} - legacy version
${blue}x-ui install${plain} - Install │
${blue}x-ui uninstall${plain} - Uninstall │
└───────────────────────────────────────────────────────┘"
echo -e "x-ui control menu usages: "
echo -e "----------------------------------------------"
echo -e "SUBCOMMANDS:"
echo -e "x-ui - Admin Management Script"
echo -e "x-ui start - Start"
echo -e "x-ui stop - Stop"
echo -e "x-ui restart - Restart"
echo -e "x-ui status - Current Status"
echo -e "x-ui settings - Current Settings"
echo -e "x-ui enable - Enable Autostart on OS Startup"
echo -e "x-ui disable - Disable Autostart on OS Startup"
echo -e "x-ui log - Check logs"
echo -e "x-ui banlog - Check Fail2ban ban logs"
echo -e "x-ui update - Update"
echo -e "x-ui custom - custom version"
echo -e "x-ui install - Install"
echo -e "x-ui uninstall - Uninstall"
echo -e "----------------------------------------------"
}
echo -e "${green}Running...${plain}"
install_base
install_x-ui $1
install_x-ui $1

77
main.go
View File

@@ -136,15 +136,6 @@ func showSetting(show bool) {
fmt.Println("get webBasePath failed, error info:", err)
}
certFile, err := settingService.GetCertFile()
if err != nil {
fmt.Println("get cert file failed, error info:", err)
}
keyFile, err := settingService.GetKeyFile()
if err != nil {
fmt.Println("get key file failed, error info:", err)
}
userService := service.UserService{}
userModel, err := userService.GetFirstUser()
if err != nil {
@@ -158,15 +149,14 @@ func showSetting(show bool) {
}
fmt.Println("current panel settings as follows:")
if certFile == "" || keyFile == "" {
fmt.Println("Warning: Panel is not secure with SSL")
} else {
fmt.Println("Panel is secure with SSL")
}
fmt.Println("username:", username)
fmt.Println("password:", userpasswd)
fmt.Println("port:", port)
fmt.Println("webBasePath:", webBasePath)
if webBasePath != "" {
fmt.Println("webBasePath:", webBasePath)
} else {
fmt.Println("webBasePath is not set")
}
}
}
@@ -226,7 +216,7 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime stri
}
}
func updateSetting(port int, username string, password string, webBasePath string, listenIP string) {
func updateSetting(port int, username string, password string, webBasePath string) {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println("Database initialization failed:", err)
@@ -262,15 +252,6 @@ func updateSetting(port int, username string, password string, webBasePath strin
fmt.Println("Base URI path set successfully")
}
}
if listenIP != "" {
err := settingService.SetListen(listenIP)
if err != nil {
fmt.Println("Failed to set listen IP:", err)
} else {
fmt.Printf("listen %v set successfully", listenIP)
}
}
}
func updateCert(publicKey string, privateKey string) {
@@ -300,37 +281,6 @@ func updateCert(publicKey string, privateKey string) {
}
}
func GetCertificate(getCert bool) {
if getCert {
settingService := service.SettingService{}
certFile, err := settingService.GetCertFile()
if err != nil {
fmt.Println("get cert file failed, error info:", err)
}
keyFile, err := settingService.GetKeyFile()
if err != nil {
fmt.Println("get key file failed, error info:", err)
}
fmt.Println("cert:", certFile)
fmt.Println("key:", keyFile)
}
}
func GetListenIP(getListen bool) {
if getListen {
settingService := service.SettingService{}
ListenIP, err := settingService.GetListen()
if err != nil {
log.Printf("Failed to retrieve listen IP: %v", err)
return
}
fmt.Println("listenIP:", ListenIP)
}
}
func migrateDb() {
inboundService := service.InboundService{}
@@ -389,8 +339,6 @@ func main() {
var username string
var password string
var webBasePath string
var listenIP string
var getListen bool
var webCertFile string
var webKeyFile string
var tgbottoken string
@@ -399,7 +347,6 @@ func main() {
var tgbotRuntime string
var reset bool
var show bool
var getCert bool
var remove_secret bool
settingCmd.BoolVar(&reset, "reset", false, "Reset all settings")
settingCmd.BoolVar(&show, "show", false, "Display current settings")
@@ -408,9 +355,6 @@ func main() {
settingCmd.StringVar(&username, "username", "", "Set login username")
settingCmd.StringVar(&password, "password", "", "Set login password")
settingCmd.StringVar(&webBasePath, "webBasePath", "", "Set base path for Panel")
settingCmd.StringVar(&listenIP, "listenIP", "", "set panel listenIP IP")
settingCmd.BoolVar(&getListen, "getListen", false, "Display current panel listenIP IP")
settingCmd.BoolVar(&getCert, "getCert", false, "Display current certificate settings")
settingCmd.StringVar(&webCertFile, "webCert", "", "Set path to public key file for panel")
settingCmd.StringVar(&webKeyFile, "webCertKey", "", "Set path to private key file for panel")
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "Set token for Telegram bot")
@@ -453,17 +397,11 @@ func main() {
if reset {
resetSetting()
} else {
updateSetting(port, username, password, webBasePath, listenIP)
updateSetting(port, username, password, webBasePath)
}
if show {
showSetting(show)
}
if getListen {
GetListenIP(getListen)
}
if getCert {
GetCertificate(getCert)
}
if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") {
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
}
@@ -484,6 +422,7 @@ func main() {
} else {
updateCert(webCertFile, webKeyFile)
}
default:
fmt.Println("Invalid subcommands")
fmt.Println()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

BIN
media/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
media/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
media/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

BIN
media/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
media/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
media/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
media/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -23,7 +23,6 @@
"destOverride": [
"http",
"tls",
"quic",
"fakedns"
],
"enabled": true
@@ -47,9 +46,7 @@
"tag": "direct",
"protocol": "freedom",
"settings": {
"domainStrategy": "AsIs",
"redirect": "",
"noises": []
"domainStrategy": "UseIP"
}
},
{

View File

@@ -92,9 +92,9 @@ func (s *Server) initRouter() (*gin.Engine, error) {
SubJsonFragment = ""
}
SubJsonNoises, err := s.settingService.GetSubJsonNoises()
SubJsonNoise, err := s.settingService.GetSubJsonNoise()
if err != nil {
SubJsonNoises = ""
SubJsonNoise = ""
}
SubJsonMux, err := s.settingService.GetSubJsonMux()
@@ -111,7 +111,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
s.sub = NewSUBController(
g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates,
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules)
SubJsonFragment, SubJsonNoise, SubJsonMux, SubJsonRules)
return engine, nil
}

View File

@@ -21,14 +21,14 @@ type SubJsonService struct {
configJson map[string]interface{}
defaultOutbounds []json_util.RawMessage
fragment string
noises string
noise string
mux string
inboundService service.InboundService
SubService *SubService
}
func NewSubJsonService(fragment string, noises string, mux string, rules string, subService *SubService) *SubJsonService {
func NewSubJsonService(fragment string, noise string, mux string, rules string, subService *SubService) *SubJsonService {
var configJson map[string]interface{}
var defaultOutbounds []json_util.RawMessage
json.Unmarshal([]byte(defaultJson), &configJson)
@@ -53,15 +53,15 @@ func NewSubJsonService(fragment string, noises string, mux string, rules string,
defaultOutbounds = append(defaultOutbounds, json_util.RawMessage(fragment))
}
if noises != "" {
defaultOutbounds = append(defaultOutbounds, json_util.RawMessage(noises))
if noise != "" {
defaultOutbounds = append(defaultOutbounds, json_util.RawMessage(noise))
}
return &SubJsonService{
configJson: configJson,
defaultOutbounds: defaultOutbounds,
fragment: fragment,
noises: noises,
noise: noise,
mux: mux,
SubService: subService,
}
@@ -217,7 +217,7 @@ func (s *SubJsonService) streamData(stream string) map[string]interface{} {
delete(streamSettings, "sockopt")
if s.fragment != "" {
streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "fragment", "tcpKeepAliveIdle": 100, "tcpMptcp": true, "penetrate": true}`)
streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "fragment", "tcpKeepAliveIdle": 100, "tcpMptcp": true, "tcpNoDelay": true}`)
}
// remove proxy protocol
@@ -268,13 +268,13 @@ func (s *SubJsonService) realityData(rData map[string]interface{}) map[string]in
rltyData["spiderX"] = "/" + random.Seq(15)
shortIds, ok := rData["shortIds"].([]interface{})
if ok && len(shortIds) > 0 {
rltyData["shortId"] = shortIds[random.Num(len(shortIds))].(string)
rltyData["shortId"] = shortIds
} else {
rltyData["shortId"] = ""
}
serverNames, ok := rData["serverNames"].([]interface{})
if ok && len(serverNames) > 0 {
rltyData["serverName"] = serverNames[random.Num(len(serverNames))].(string)
rltyData["serverName"] = serverNames
} else {
rltyData["serverName"] = ""
}

View File

@@ -208,6 +208,17 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
headers, _ := ws["headers"].(map[string]interface{})
obj["host"] = searchHost(headers)
}
case "http":
obj["net"] = "h2"
http, _ := stream["httpSettings"].(map[string]interface{})
obj["path"], _ = http["path"].(string)
obj["host"] = searchHost(http)
case "quic":
quic, _ := stream["quicSettings"].(map[string]interface{})
header := quic["header"].(map[string]interface{})
obj["type"], _ = header["type"].(string)
obj["host"], _ = quic["security"].(string)
obj["path"], _ = quic["key"].(string)
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
obj["path"] = grpc["serviceName"].(string)
@@ -224,16 +235,15 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
headers, _ := httpupgrade["headers"].(map[string]interface{})
obj["host"] = searchHost(headers)
}
case "xhttp":
xhttp, _ := stream["xhttpSettings"].(map[string]interface{})
obj["path"] = xhttp["path"].(string)
if host, ok := xhttp["host"].(string); ok && len(host) > 0 {
case "splithttp":
splithttp, _ := stream["splithttpSettings"].(map[string]interface{})
obj["path"] = splithttp["path"].(string)
if host, ok := splithttp["host"].(string); ok && len(host) > 0 {
obj["host"] = host
} else {
headers, _ := xhttp["headers"].(map[string]interface{})
headers, _ := splithttp["headers"].(map[string]interface{})
obj["host"] = searchHost(headers)
}
obj["mode"] = xhttp["mode"].(string)
}
security, _ := stream["security"].(string)
obj["tls"] = security
@@ -356,6 +366,16 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
headers, _ := ws["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
}
case "http":
http, _ := stream["httpSettings"].(map[string]interface{})
params["path"] = http["path"].(string)
params["host"] = searchHost(http)
case "quic":
quic, _ := stream["quicSettings"].(map[string]interface{})
params["quicSecurity"] = quic["security"].(string)
params["key"] = quic["key"].(string)
header := quic["header"].(map[string]interface{})
params["headerType"] = header["type"].(string)
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
params["serviceName"] = grpc["serviceName"].(string)
@@ -372,16 +392,15 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
headers, _ := httpupgrade["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
}
case "xhttp":
xhttp, _ := stream["xhttpSettings"].(map[string]interface{})
params["path"] = xhttp["path"].(string)
if host, ok := xhttp["host"].(string); ok && len(host) > 0 {
case "splithttp":
splithttp, _ := stream["splithttpSettings"].(map[string]interface{})
params["path"] = splithttp["path"].(string)
if host, ok := splithttp["host"].(string); ok && len(host) > 0 {
params["host"] = host
} else {
headers, _ := xhttp["headers"].(map[string]interface{})
headers, _ := splithttp["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
}
params["mode"] = xhttp["mode"].(string)
}
security, _ := stream["security"].(string)
if security == "tls" {
@@ -445,7 +464,38 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
}
}
if security != "tls" && security != "reality" {
if security == "xtls" {
params["security"] = "xtls"
xtlsSetting, _ := stream["xtlsSettings"].(map[string]interface{})
alpns, _ := xtlsSetting["alpn"].([]interface{})
var alpn []string
for _, a := range alpns {
alpn = append(alpn, a.(string))
}
if len(alpn) > 0 {
params["alpn"] = strings.Join(alpn, ",")
}
if sniValue, ok := searchKey(xtlsSetting, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
xtlsSettings, _ := searchKey(xtlsSetting, "settings")
if xtlsSetting != nil {
if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
}
if insecure, ok := searchKey(xtlsSettings, "allowInsecure"); ok {
if insecure.(bool) {
params["allowInsecure"] = "1"
}
}
}
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
params["flow"] = clients[clientIndex].Flow
}
}
if security != "tls" && security != "reality" && security != "xtls" {
params["security"] = "none"
}
@@ -550,6 +600,16 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
headers, _ := ws["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
}
case "http":
http, _ := stream["httpSettings"].(map[string]interface{})
params["path"] = http["path"].(string)
params["host"] = searchHost(http)
case "quic":
quic, _ := stream["quicSettings"].(map[string]interface{})
params["quicSecurity"] = quic["security"].(string)
params["key"] = quic["key"].(string)
header := quic["header"].(map[string]interface{})
params["headerType"] = header["type"].(string)
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
params["serviceName"] = grpc["serviceName"].(string)
@@ -566,16 +626,15 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
headers, _ := httpupgrade["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
}
case "xhttp":
xhttp, _ := stream["xhttpSettings"].(map[string]interface{})
params["path"] = xhttp["path"].(string)
if host, ok := xhttp["host"].(string); ok && len(host) > 0 {
case "splithttp":
splithttp, _ := stream["splithttpSettings"].(map[string]interface{})
params["path"] = splithttp["path"].(string)
if host, ok := splithttp["host"].(string); ok && len(host) > 0 {
params["host"] = host
} else {
headers, _ := xhttp["headers"].(map[string]interface{})
headers, _ := splithttp["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
}
params["mode"] = xhttp["mode"].(string)
}
security, _ := stream["security"].(string)
if security == "tls" {
@@ -635,7 +694,39 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
}
}
if security != "tls" && security != "reality" {
if security == "xtls" {
params["security"] = "xtls"
xtlsSetting, _ := stream["xtlsSettings"].(map[string]interface{})
alpns, _ := xtlsSetting["alpn"].([]interface{})
var alpn []string
for _, a := range alpns {
alpn = append(alpn, a.(string))
}
if len(alpn) > 0 {
params["alpn"] = strings.Join(alpn, ",")
}
if sniValue, ok := searchKey(xtlsSetting, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
xtlsSettings, _ := searchKey(xtlsSetting, "settings")
if xtlsSetting != nil {
if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
}
if insecure, ok := searchKey(xtlsSettings, "allowInsecure"); ok {
if insecure.(bool) {
params["allowInsecure"] = "1"
}
}
}
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
params["flow"] = clients[clientIndex].Flow
}
}
if security != "tls" && security != "reality" && security != "xtls" {
params["security"] = "none"
}
@@ -744,6 +835,16 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
headers, _ := ws["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
}
case "http":
http, _ := stream["httpSettings"].(map[string]interface{})
params["path"] = http["path"].(string)
params["host"] = searchHost(http)
case "quic":
quic, _ := stream["quicSettings"].(map[string]interface{})
params["quicSecurity"] = quic["security"].(string)
params["key"] = quic["key"].(string)
header := quic["header"].(map[string]interface{})
params["headerType"] = header["type"].(string)
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
params["serviceName"] = grpc["serviceName"].(string)
@@ -760,16 +861,15 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
headers, _ := httpupgrade["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
}
case "xhttp":
xhttp, _ := stream["xhttpSettings"].(map[string]interface{})
params["path"] = xhttp["path"].(string)
if host, ok := xhttp["host"].(string); ok && len(host) > 0 {
case "splithttp":
splithttp, _ := stream["splithttpSettings"].(map[string]interface{})
params["path"] = splithttp["path"].(string)
if host, ok := splithttp["host"].(string); ok && len(host) > 0 {
params["host"] = host
} else {
headers, _ := xhttp["headers"].(map[string]interface{})
headers, _ := splithttp["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
}
params["mode"] = xhttp["mode"].(string)
}
security, _ := stream["security"].(string)

View File

@@ -4,14 +4,18 @@ import (
"fmt"
)
func FormatTraffic(trafficBytes int64) string {
units := []string{"B", "KB", "MB", "GB", "TB", "PB"}
unitIndex := 0
size := float64(trafficBytes)
for size >= 1024 && unitIndex < len(units)-1 {
size /= 1024
unitIndex++
func FormatTraffic(trafficBytes int64) (size string) {
if trafficBytes < 1024 {
return fmt.Sprintf("%.2fB", float64(trafficBytes)/float64(1))
} else if trafficBytes < (1024 * 1024) {
return fmt.Sprintf("%.2fKB", float64(trafficBytes)/float64(1024))
} else if trafficBytes < (1024 * 1024 * 1024) {
return fmt.Sprintf("%.2fMB", float64(trafficBytes)/float64(1024*1024))
} else if trafficBytes < (1024 * 1024 * 1024 * 1024) {
return fmt.Sprintf("%.2fGB", float64(trafficBytes)/float64(1024*1024*1024))
} else if trafficBytes < (1024 * 1024 * 1024 * 1024 * 1024) {
return fmt.Sprintf("%.2fTB", float64(trafficBytes)/float64(1024*1024*1024*1024))
} else {
return fmt.Sprintf("%.2fEB", float64(trafficBytes)/float64(1024*1024*1024*1024*1024))
}
return fmt.Sprintf("%.2f%s", size, units[unitIndex])
}

File diff suppressed because one or more lines are too long

View File

@@ -63,7 +63,7 @@
return scriptHint(editor, javascriptKeywords,
function (e, cur) {return e.getTokenAt(cur);},
options);
}
};
CodeMirror.registerHelper("hint", "javascript", javascriptHint);
function getCoffeeScriptToken(editor, cur) {

View File

@@ -362,7 +362,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (type == wanted) return cont();
else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass();
else return cont(exp);
}
};
return exp;
}

View File

@@ -6,7 +6,7 @@
.CodeMirror-lint-tooltip {
background-color: #ffd;
border: 1px solid black;
border-radius: 4px;
border-radius: 4px 4px 4px 4px;
color: black;
font-family: monospace;
font-size: 10pt;

File diff suppressed because one or more lines are too long

View File

@@ -1,103 +1,93 @@
const supportLangs = [
{
name: "English",
value: "en-US",
icon: "🇺🇸",
},
{
name: "فارسی",
value: "fa-IR",
icon: "🇮🇷",
},
{
name: "简体中文",
value: "zh-CN",
icon: "🇨🇳",
},
{
name: "繁體中文",
value: "zh-TW",
icon: "🇹🇼",
},
{
name: "日本語",
value: "ja-JP",
icon: "🇯🇵",
},
{
name: "Русский",
value: "ru-RU",
icon: "🇷🇺",
},
{
name: "Tiếng Việt",
value: "vi-VN",
icon: "🇻🇳",
},
{
name: "Español",
value: "es-ES",
icon: "🇪🇸",
},
{
name: "Indonesian",
value: "id-ID",
icon: "🇮🇩",
},
{
name: "Український",
value: "uk-UA",
icon: "🇺🇦",
},
{
name: "Türkçe",
value: "tr-TR",
icon: "🇹🇷",
},
{
name: "Português",
value: "pt-BR",
icon: "🇧🇷",
},
{
name: 'English',
value: 'en-US',
icon: '🇺🇸',
},
{
name: 'فارسی',
value: 'fa-IR',
icon: '🇮🇷',
},
{
name: '中文',
value: 'zh-CN',
icon: '🇨🇳',
},
{
name: 'Русский',
value: 'ru-RU',
icon: '🇷🇺',
},
{
name: 'Tiếng Việt',
value: 'vi-VN',
icon: '🇻🇳',
},
{
name: 'Español',
value: 'es-ES',
icon: '🇪🇸',
},
{
name: 'Indonesian',
value: 'id-ID',
icon: '🇮🇩',
},
{
name: 'Український',
value: 'uk-UA',
icon: '🇺🇦',
},
{
name: 'Türkçe',
value: 'tr-TR',
icon: '🇹🇷',
},
{
name: "Português",
value: "pt-BR",
icon: "🇧🇷",
},
];
function getLang() {
let lang = getCookie("lang");
let lang = getCookie('lang');
if (!lang) {
if (window.navigator) {
lang = window.navigator.language || window.navigator.userLanguage;
if (!lang) {
if (window.navigator) {
lang = window.navigator.language || window.navigator.userLanguage;
if (isSupportLang(lang)) {
setCookie("lang", lang, 150);
} else {
setCookie("lang", "en-US", 150);
window.location.reload();
}
} else {
setCookie("lang", "en-US", 150);
window.location.reload();
}
}
if (isSupportLang(lang)) {
setCookie('lang', lang, 150);
} else {
setCookie('lang', 'en-US', 150);
window.location.reload();
}
} else {
setCookie('lang', 'en-US', 150);
window.location.reload();
}
}
return lang;
return lang;
}
function setLang(lang) {
if (!isSupportLang(lang)) {
lang = "en-US";
}
if (!isSupportLang(lang)) {
lang = 'en-US';
}
setCookie("lang", lang, 150);
window.location.reload();
setCookie('lang', lang, 150);
window.location.reload();
}
function isSupportLang(lang) {
for (l of supportLangs) {
if (l.value === lang) {
return true;
}
}
for (l of supportLangs) {
if (l.value === lang) {
return true;
}
}
return false;
return false;
}

View File

@@ -140,9 +140,9 @@ class DBInbound {
return false;
}
}
genInboundLinks(remarkModel) {
genInboundLinks(remarkModel) {
const inbound = this.toInbound();
return inbound.genInboundLinks(this.remark, remarkModel);
return inbound.genInboundLinks(this.remark,remarkModel);
}
}

View File

@@ -39,7 +39,6 @@ const UTLS_FINGERPRINT = {
UTLS_QQ: "qq",
UTLS_RANDOM: "random",
UTLS_RANDOMIZED: "randomized",
UTLS_UNSAFE: "unsafe",
};
const ALPN_OPTION = {
@@ -78,13 +77,6 @@ const USERS_SECURITY = {
ZERO: "zero",
};
const MODE_OPTION = {
AUTO: "auto",
PACKET_UP: "packet-up",
STREAM_UP: "stream-up",
STREAM_ONE: "stream-one",
};
Object.freeze(Protocols);
Object.freeze(SSMethods);
Object.freeze(TLS_FLOW_CONTROL);
@@ -93,7 +85,6 @@ Object.freeze(ALPN_OPTION);
Object.freeze(OutboundDomainStrategies);
Object.freeze(WireguardDomainStrategy);
Object.freeze(USERS_SECURITY);
Object.freeze(MODE_OPTION);
class CommonClass {
@@ -207,23 +198,16 @@ class KcpStreamSettings extends CommonClass {
}
class WsStreamSettings extends CommonClass {
constructor(
path = '/',
host = '',
heartbeatPeriod = 0,
) {
constructor(path = '/', host = '') {
super();
this.path = path;
this.host = host;
this.heartbeatPeriod = heartbeatPeriod;
}
static fromJson(json = {}) {
return new WsStreamSettings(
json.path,
json.host,
json.heartbeatPeriod,
);
}
@@ -231,11 +215,63 @@ class WsStreamSettings extends CommonClass {
return {
path: this.path,
host: this.host,
heartbeatPeriod: this.heartbeatPeriod
};
}
}
class HttpStreamSettings extends CommonClass {
constructor(path = '/', host = '') {
super();
this.path = path;
this.host = host;
}
static fromJson(json = {}) {
return new HttpStreamSettings(
json.path,
json.host ? json.host.join(',') : '',
);
}
toJson() {
return {
path: this.path,
host: ObjectUtil.isEmpty(this.host) ? [''] : this.host.split(','),
}
}
}
class QuicStreamSettings extends CommonClass {
constructor(
security = 'none',
key = '',
type = 'none'
) {
super();
this.security = security;
this.key = key;
this.type = type;
}
static fromJson(json = {}) {
return new QuicStreamSettings(
json.security,
json.key,
json.header ? json.header.type : 'none',
);
}
toJson() {
return {
security: this.security,
key: this.key,
header: {
type: this.type,
}
}
}
}
class GrpcStreamSettings extends CommonClass {
constructor(
serviceName = "",
@@ -283,39 +319,17 @@ class HttpUpgradeStreamSettings extends CommonClass {
}
}
class xHTTPStreamSettings extends CommonClass {
constructor(
path = '/',
host = '',
mode = '',
noGRPCHeader = false,
scMinPostsIntervalMs = "30",
xmux = {
maxConcurrency: "16-32",
maxConnections: 0,
cMaxReuseTimes: 0,
hMaxRequestTimes: "600-900",
hMaxReusableSecs: "1800-3000",
hKeepAlivePeriod: 0,
},
) {
class SplitHTTPStreamSettings extends CommonClass {
constructor(path = '/', host = '') {
super();
this.path = path;
this.host = host;
this.mode = mode;
this.noGRPCHeader = noGRPCHeader;
this.scMinPostsIntervalMs = scMinPostsIntervalMs;
this.xmux = xmux;
}
static fromJson(json = {}) {
return new xHTTPStreamSettings(
return new SplitHTTPStreamSettings(
json.path,
json.host,
json.mode,
json.noGRPCHeader,
json.scMinPostsIntervalMs,
json.xmux
);
}
@@ -323,17 +337,6 @@ class xHTTPStreamSettings extends CommonClass {
return {
path: this.path,
host: this.host,
mode: this.mode,
noGRPCHeader: this.noGRPCHeader,
scMinPostsIntervalMs: this.scMinPostsIntervalMs,
xmux: {
maxConcurrency: this.xmux.maxConcurrency,
maxConnections: this.xmux.maxConnections,
cMaxReuseTimes: this.xmux.cMaxReuseTimes,
hMaxRequestTimes: this.xmux.hMaxRequestTimes,
hMaxReusableSecs: this.xmux.hMaxReusableSecs,
hKeepAlivePeriod: this.xmux.hKeepAlivePeriod,
},
};
}
}
@@ -411,14 +414,14 @@ class SockoptStreamSettings extends CommonClass {
tcpFastOpen = false,
tcpKeepAliveInterval = 0,
tcpMptcp = false,
penetrate = false
tcpNoDelay = false
) {
super();
this.dialerProxy = dialerProxy;
this.tcpFastOpen = tcpFastOpen;
this.tcpKeepAliveInterval = tcpKeepAliveInterval;
this.tcpMptcp = tcpMptcp;
this.penetrate = penetrate;
this.tcpNoDelay = tcpNoDelay;
}
static fromJson(json = {}) {
@@ -428,7 +431,7 @@ class SockoptStreamSettings extends CommonClass {
json.tcpFastOpen,
json.tcpKeepAliveInterval,
json.tcpMptcp,
json.penetrate,
json.tcpNoDelay,
);
}
@@ -438,7 +441,7 @@ class SockoptStreamSettings extends CommonClass {
tcpFastOpen: this.tcpFastOpen,
tcpKeepAliveInterval: this.tcpKeepAliveInterval,
tcpMptcp: this.tcpMptcp,
penetrate: this.penetrate,
tcpNoDelay: this.tcpNoDelay,
};
}
}
@@ -452,9 +455,11 @@ class StreamSettings extends CommonClass {
tcpSettings = new TcpStreamSettings(),
kcpSettings = new KcpStreamSettings(),
wsSettings = new WsStreamSettings(),
httpSettings = new HttpStreamSettings(),
quicSettings = new QuicStreamSettings(),
grpcSettings = new GrpcStreamSettings(),
httpupgradeSettings = new HttpUpgradeStreamSettings(),
xhttpSettings = new xHTTPStreamSettings(),
splithttpSettings = new SplitHTTPStreamSettings(),
sockopt = undefined,
) {
super();
@@ -465,9 +470,11 @@ class StreamSettings extends CommonClass {
this.tcp = tcpSettings;
this.kcp = kcpSettings;
this.ws = wsSettings;
this.http = httpSettings;
this.quic = quicSettings;
this.grpc = grpcSettings;
this.httpupgrade = httpupgradeSettings;
this.xhttp = xhttpSettings;
this.splithttp = splithttpSettings;
this.sockopt = sockopt;
}
@@ -496,9 +503,11 @@ class StreamSettings extends CommonClass {
TcpStreamSettings.fromJson(json.tcpSettings),
KcpStreamSettings.fromJson(json.kcpSettings),
WsStreamSettings.fromJson(json.wsSettings),
HttpStreamSettings.fromJson(json.httpSettings),
QuicStreamSettings.fromJson(json.quicSettings),
GrpcStreamSettings.fromJson(json.grpcSettings),
HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
xHTTPStreamSettings.fromJson(json.xhttpSettings),
SplitHTTPStreamSettings.fromJson(json.splithttpSettings),
SockoptStreamSettings.fromJson(json.sockopt),
);
}
@@ -513,9 +522,11 @@ class StreamSettings extends CommonClass {
tcpSettings: network === 'tcp' ? this.tcp.toJson() : undefined,
kcpSettings: network === 'kcp' ? this.kcp.toJson() : undefined,
wsSettings: network === 'ws' ? this.ws.toJson() : undefined,
httpSettings: network === 'http' ? this.http.toJson() : undefined,
quicSettings: network === 'quic' ? this.quic.toJson() : undefined,
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,
xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined,
splithttpSettings: network === 'splithttp' ? this.splithttp.toJson() : undefined,
sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
};
}
@@ -553,7 +564,7 @@ class Mux extends CommonClass {
class Outbound extends CommonClass {
constructor(
tag = '',
protocol = Protocols.VLESS,
protocol = Protocols.VMess,
settings = null,
streamSettings = new StreamSettings(),
sendThrough,
@@ -580,7 +591,7 @@ class Outbound extends CommonClass {
canEnableTls() {
if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol)) return false;
return ["tcp", "ws", "http", "grpc", "httpupgrade", "xhttp"].includes(this.stream.network);
return ["tcp", "ws", "http", "quic", "grpc", "httpupgrade", "splithttp"].includes(this.stream.network);
}
//this is used for xtls-rprx-vision
@@ -593,7 +604,7 @@ class Outbound extends CommonClass {
canEnableReality() {
if (![Protocols.VLESS, Protocols.Trojan].includes(this.protocol)) return false;
return ["tcp", "http", "grpc", "xhttp"].includes(this.stream.network);
return ["tcp", "http", "grpc"].includes(this.stream.network);
}
canEnableStream() {
@@ -691,12 +702,22 @@ class Outbound extends CommonClass {
stream.seed = json.path;
} else if (network === 'ws') {
stream.ws = new WsStreamSettings(json.path, json.host);
} else if (network === 'http' || network == 'h2') {
stream.network = 'http'
stream.http = new HttpStreamSettings(
json.path,
json.host);
} else if (network === 'quic') {
stream.quic = new QuicStreamSettings(
json.host ? json.host : 'none',
json.path,
json.type ? json.type : 'none');
} else if (network === 'grpc') {
stream.grpc = new GrpcStreamSettings(json.path, json.authority, json.type == 'multi');
} else if (network === 'httpupgrade') {
stream.httpupgrade = new HttpUpgradeStreamSettings(json.path, json.host);
} else if (network === 'xhttp') {
stream.xhttp = new xHTTPStreamSettings(json.path, json.host, json.mode);
} else if (network === 'splithttp') {
stream.splithttp = new SplitHTTPStreamSettings(json.path, json.host);
}
if (json.tls && json.tls == 'tls') {
@@ -721,7 +742,6 @@ class Outbound extends CommonClass {
let headerType = url.searchParams.get('headerType') ?? undefined;
let host = url.searchParams.get('host') ?? undefined;
let path = url.searchParams.get('path') ?? undefined;
let mode = url.searchParams.get('mode') ?? undefined;
if (type === 'tcp' || type === 'none') {
stream.tcp = new TcpStreamSettings(headerType ?? 'none', host, path);
@@ -731,6 +751,13 @@ class Outbound extends CommonClass {
stream.kcp.seed = path;
} else if (type === 'ws') {
stream.ws = new WsStreamSettings(path, host);
} else if (type === 'http' || type == 'h2') {
stream.http = new HttpStreamSettings(path, host);
} else if (type === 'quic') {
stream.quic = new QuicStreamSettings(
url.searchParams.get('quicSecurity') ?? 'none',
url.searchParams.get('key') ?? '',
headerType ?? 'none');
} else if (type === 'grpc') {
stream.grpc = new GrpcStreamSettings(
url.searchParams.get('serviceName') ?? '',
@@ -738,8 +765,8 @@ class Outbound extends CommonClass {
url.searchParams.get('mode') == 'multi');
} else if (type === 'httpupgrade') {
stream.httpupgrade = new HttpUpgradeStreamSettings(path, host);
} else if (type === 'xhttp') {
stream.xhttp = new xHTTPStreamSettings(path, host, mode);
} else if (type === 'splithttp') {
stream.splithttp = new SplitHTTPStreamSettings(path, host);
}
if (security == 'tls') {
@@ -836,50 +863,41 @@ Outbound.Settings = class extends CommonClass {
Outbound.FreedomSettings = class extends CommonClass {
constructor(
domainStrategy = '',
timeout = '',
redirect = '',
fragment = {},
noises = []
noise = {}
) {
super();
this.domainStrategy = domainStrategy;
this.timeout = timeout;
this.redirect = redirect;
this.fragment = fragment;
this.noises = noises;
}
addNoise() {
this.noises.push(new Outbound.FreedomSettings.Noise());
}
delNoise(index) {
this.noises.splice(index, 1);
this.noise = noise;
}
static fromJson(json = {}) {
return new Outbound.FreedomSettings(
json.domainStrategy,
json.timeout,
json.redirect,
json.fragment ? Outbound.FreedomSettings.Fragment.fromJson(json.fragment) : undefined,
json.noises ? json.noises.map(noise => Outbound.FreedomSettings.Noise.fromJson(noise)) : undefined,
json.noise ? Outbound.FreedomSettings.Noise.fromJson(json.noise) : undefined,
);
}
toJson() {
return {
domainStrategy: ObjectUtil.isEmpty(this.domainStrategy) ? undefined : this.domainStrategy,
redirect: ObjectUtil.isEmpty(this.redirect) ? undefined: this.redirect,
timeout: this.timeout,
redirect: this.redirect,
fragment: Object.keys(this.fragment).length === 0 ? undefined : this.fragment,
noises: this.noises.length === 0 ? undefined : Outbound.FreedomSettings.Noise.toJsonArray(this.noises),
noise: Object.keys(this.noise).length === 0 ? undefined : this.noise,
};
}
};
Outbound.FreedomSettings.Fragment = class extends CommonClass {
constructor(
packets = '1-3',
length = '',
interval = ''
) {
constructor(packets = '1-3', length = '', interval = '') {
super();
this.packets = packets;
this.length = length;
@@ -894,34 +912,19 @@ Outbound.FreedomSettings.Fragment = class extends CommonClass {
);
}
};
Outbound.FreedomSettings.Noise = class extends CommonClass {
constructor(
type = 'rand',
packet = '10-20',
delay = '10-16'
) {
constructor(packet = '', delay = '') {
super();
this.type = type;
this.packet = packet;
this.delay = delay;
}
static fromJson(json = {}) {
return new Outbound.FreedomSettings.Noise(
json.type,
json.packet,
json.delay,
);
}
toJson() {
return {
type: this.type,
packet: this.packet,
delay: this.delay,
};
}
};
Outbound.BlackholeSettings = class extends CommonClass {
@@ -943,19 +946,11 @@ Outbound.BlackholeSettings = class extends CommonClass {
}
};
Outbound.DNSSettings = class extends CommonClass {
constructor(
network = 'udp',
address = '',
port = 53,
nonIPQuery = 'drop',
blockTypes = []
) {
constructor(network = 'udp', address = '1.1.1.1', port = 53) {
super();
this.network = network;
this.address = address;
this.port = port;
this.nonIPQuery = nonIPQuery;
this.blockTypes = blockTypes;
}
static fromJson(json = {}) {
@@ -963,8 +958,6 @@ Outbound.DNSSettings = class extends CommonClass {
json.network,
json.address,
json.port,
json.nonIPQuery,
json.blockTypes,
);
}
};
@@ -1163,7 +1156,7 @@ Outbound.WireguardSettings = class extends CommonClass {
domainStrategy = '',
reserved = '',
peers = [new Outbound.WireguardSettings.Peer()],
noKernelTun = false,
kernelMode = false
) {
super();
this.mtu = mtu;
@@ -1174,7 +1167,7 @@ Outbound.WireguardSettings = class extends CommonClass {
this.domainStrategy = domainStrategy;
this.reserved = Array.isArray(reserved) ? reserved.join(',') : reserved;
this.peers = peers;
this.noKernelTun = noKernelTun;
this.kernelMode = kernelMode;
}
addPeer() {
@@ -1194,7 +1187,7 @@ Outbound.WireguardSettings = class extends CommonClass {
json.domainStrategy,
json.reserved,
json.peers.map(peer => Outbound.WireguardSettings.Peer.fromJson(peer)),
json.noKernelTun,
json.kernelMode,
);
}
@@ -1207,7 +1200,7 @@ Outbound.WireguardSettings = class extends CommonClass {
domainStrategy: WireguardDomainStrategy.includes(this.domainStrategy) ? this.domainStrategy : undefined,
reserved: this.reserved ? this.reserved.split(",").map(Number) : undefined,
peers: Outbound.WireguardSettings.Peer.toJsonArray(this.peers),
noKernelTun: this.noKernelTun,
kernelMode: this.kernelMode,
};
}
};

View File

@@ -7,7 +7,7 @@ class AllSetting {
this.webCertFile = "";
this.webKeyFile = "";
this.webBasePath = "/";
this.sessionMaxAge = 60;
this.sessionMaxAge = 0;
this.pageSize = 50;
this.expireDiff = 0;
this.trafficDiff = 0;
@@ -16,7 +16,6 @@ class AllSetting {
this.tgBotEnable = false;
this.tgBotToken = "";
this.tgBotProxy = "";
this.tgBotAPIServer = "";
this.tgBotChatId = "";
this.tgRunTime = "@daily";
this.tgBotBackup = false;
@@ -26,7 +25,6 @@ class AllSetting {
this.xrayTemplateConfig = "";
this.secretEnable = false;
this.subEnable = false;
this.subSyncEnable = true;
this.subListen = "";
this.subPort = 2096;
this.subPath = "/sub/";
@@ -40,11 +38,11 @@ class AllSetting {
this.subURI = "";
this.subJsonURI = "";
this.subJsonFragment = "";
this.subJsonNoises = "";
this.subJsonNoise = "";
this.subJsonMux = "";
this.subJsonRules = "";
this.timeLocation = "Local";
this.timeLocation = "Asia/Tehran";
if (data == null) {
return

View File

@@ -5,9 +5,9 @@ const ONE_TB = ONE_GB * 1024;
const ONE_PB = ONE_TB * 1024;
function sizeFormat(size) {
if (size <= 0) return "0 B";
if (size < ONE_KB) {
if (size < 0) {
return "0 B";
} else if (size < ONE_KB) {
return size.toFixed(0) + " B";
} else if (size < ONE_MB) {
return (size / ONE_KB).toFixed(2) + " KB";
@@ -59,7 +59,7 @@ function formatSecond(second) {
return (second / 3600).toFixed(0) + 'h';
} else {
day = Math.floor(second / 3600 / 24);
remain = ((second / 3600) - (day * 24)).toFixed(0);
remain = ((second/3600) - (day*24)).toFixed(0);
return day + 'd' + (remain > 0 ? ' ' + remain + 'h' : '');
}
}
@@ -149,7 +149,7 @@ function userExpiryColor(threshold, client, isDark = false) {
return isDark ? '#2c3950' : '#bcbcbc';
}
now = new Date().getTime(),
expiry = client.expiryTime;
expiry = client.expiryTime;
switch (true) {
case expiry === null:
return "#7a316f"; // purple

View File

@@ -70,41 +70,6 @@ class HttpUtil {
}
return msg;
}
static async jsonPost(url, data) {
let msg;
try {
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
};
const resp = await fetch(basePath + url.replace(/^\/+|\/+$/g, ''), requestOptions);
const response = await resp.json();
msg = this._respToMsg({data : response});
} catch (e) {
msg = new Msg(false, e.toString());
}
this._handleMsg(msg);
return msg;
}
static async postWithModalJson(url, data, modal) {
if (modal) {
modal.loading(true);
}
const msg = await this.jsonPost(url, data);
if (modal) {
modal.loading(false);
if (msg instanceof Msg && msg.success) {
modal.close();
}
}
return msg;
}
}
class PromiseUtil {

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +1,10 @@
package controller
import (
"errors"
"encoding/json"
"fmt"
"strconv"
"x-ui/database/model"
"x-ui/web/service"
"x-ui/web/session"
@@ -33,13 +33,9 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/clientIps/:email", a.getClientIps)
g.POST("/clearClientIps/:email", a.clearClientIps)
g.POST("/addClient", a.addInboundClient)
g.POST("/addGroupClient", a.addGroupInboundClient)
g.POST("/:id/delClient/:clientId", a.delInboundClient)
g.POST("/delGroupClients", a.delGroupClients)
g.POST("/updateClient/:clientId", a.updateInboundClient)
g.POST("/updateClients", a.updateGroupInboundClient)
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
g.POST("/resetGroupClientTraffic", a.resetGroupClientTraffic)
g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients)
@@ -194,34 +190,6 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
}
}
func (a *InboundController) addGroupInboundClient(c *gin.Context) {
var requestData []model.Inbound
err := c.ShouldBindJSON(&requestData)
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
needRestart := true
for _, data := range requestData {
needRestart, err = a.inboundService.AddInboundClient(&data)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
}
jsonMsg(c, "Client(s) added", nil)
if err == nil && needRestart {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) delInboundClient(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
@@ -243,38 +211,6 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
}
}
func (a *InboundController) delGroupClients(c *gin.Context) {
var requestData []struct {
InboundID int `json:"inboundId"`
ClientID string `json:"clientId"`
}
if err := c.ShouldBindJSON(&requestData); err != nil {
jsonMsg(c, "Invalid request data", err)
return
}
needRestart := false
for _, req := range requestData {
needRestartTmp, err := a.inboundService.DelInboundClient(req.InboundID, req.ClientID)
if err != nil {
jsonMsg(c, "Failed to delete client", err)
return
}
if needRestartTmp {
needRestart = true
}
}
jsonMsg(c, "Clients deleted successfully", nil)
if needRestart {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) updateInboundClient(c *gin.Context) {
clientId := c.Param("clientId")
@@ -298,56 +234,6 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
}
}
func (a *InboundController) updateGroupInboundClient(c *gin.Context) {
var requestData []map[string]interface{}
if err := c.ShouldBindJSON(&requestData); err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
needRestart := false
for _, item := range requestData {
inboundMap, ok := item["inbound"].(map[string]interface{})
if !ok {
jsonMsg(c, "Something went wrong!", errors.New("Failed to convert 'inbound' to map"))
return
}
clientId, ok := item["clientId"].(string)
if !ok {
jsonMsg(c, "Something went wrong!", errors.New("Failed to convert 'clientId' to string"))
return
}
inboundJSON, err := json.Marshal(inboundMap)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
var inboundModel model.Inbound
if err := json.Unmarshal(inboundJSON, &inboundModel); err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
if restart, err := a.inboundService.UpdateInboundClient(&inboundModel, clientId); err != nil {
jsonMsg(c, "Something went wrong!", err)
return
} else {
needRestart = needRestart || restart
}
}
jsonMsg(c, "Client updated", nil)
if needRestart {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) resetClientTraffic(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
@@ -367,44 +253,6 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
}
}
func (a *InboundController) resetGroupClientTraffic(c *gin.Context) {
var requestData []struct {
InboundID int `json:"inboundId"` // Map JSON "inboundId" to struct field "InboundID"
Email string `json:"email"` // Map JSON "email" to struct field "Email"
}
// Parse JSON body directly using ShouldBindJSON
if err := c.ShouldBindJSON(&requestData); err != nil {
jsonMsg(c, "Invalid request data", err)
return
}
needRestart := false
// Process each request data
for _, req := range requestData {
needRestartTmp, err := a.inboundService.ResetClientTraffic(req.InboundID, req.Email)
if err != nil {
jsonMsg(c, "Failed to reset client traffic", err)
return
}
// If any request requires a restart, set needRestart to true
if needRestartTmp {
needRestart = true
}
}
// Send response back to the client
jsonMsg(c, "Traffic reset for all clients", nil)
// Restart the service if required
if needRestart {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) resetAllTraffics(c *gin.Context) {
err := a.inboundService.ResetAllTraffics()
if err != nil {

View File

@@ -9,7 +9,6 @@ import (
"x-ui/web/service"
"x-ui/web/session"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
@@ -50,8 +49,8 @@ func (a *IndexController) index(c *gin.Context) {
func (a *IndexController) login(c *gin.Context) {
var form LoginForm
if err := c.ShouldBind(&form); err != nil {
err := c.ShouldBind(&form)
if err != nil {
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.invalidFormData"))
return
}
@@ -69,31 +68,33 @@ func (a *IndexController) login(c *gin.Context) {
safeUser := template.HTMLEscapeString(form.Username)
safePass := template.HTMLEscapeString(form.Password)
safeSecret := template.HTMLEscapeString(form.LoginSecret)
if user == nil {
logger.Warningf("wrong username: \"%s\", password: \"%s\", secret: \"%s\", IP: \"%s\"", safeUser, safePass, safeSecret, getRemoteIp(c))
logger.Warningf("wrong username or password or secret: \"%s\" \"%s\" \"%s\"", safeUser, safePass, safeSecret)
a.tgbot.UserLoginNotify(safeUser, safePass, getRemoteIp(c), timeStr, 0)
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
return
} else {
logger.Infof("%s logged in successfully, Ip Address: %s\n", safeUser, getRemoteIp(c))
a.tgbot.UserLoginNotify(safeUser, ``, getRemoteIp(c), timeStr, 1)
}
logger.Infof("%s logged in successfully, Ip Address: %s\n", safeUser, getRemoteIp(c))
a.tgbot.UserLoginNotify(safeUser, ``, getRemoteIp(c), timeStr, 1)
sessionMaxAge, err := a.settingService.GetSessionMaxAge()
if err != nil {
logger.Warning("Unable to get session's max age from DB")
}
session.SetMaxAge(c, sessionMaxAge*60)
session.SetLoginUser(c, user)
if err := sessions.Default(c).Save(); err != nil {
logger.Warning("Unable to save session: ", err)
return
if sessionMaxAge <= 0 {
sessionMaxAge = 60
}
logger.Infof("%s logged in successfully", safeUser)
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), nil)
err = session.SetMaxAge(c, sessionMaxAge*60)
if err != nil {
logger.Warning("Unable to set session's max age")
}
err = session.SetLoginUser(c, user)
logger.Infof("%s logged in successfully", user.Username)
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), err)
}
func (a *IndexController) logout(c *gin.Context) {
@@ -102,9 +103,6 @@ func (a *IndexController) logout(c *gin.Context) {
logger.Infof("%s logged out successfully", user.Username)
}
session.ClearSession(c)
if err := sessions.Default(c).Save(); err != nil {
logger.Warning("Unable to save session after clearing:", err)
}
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
}

View File

@@ -42,12 +42,12 @@ func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
if err == nil {
m.Success = true
if msg != "" {
m.Msg = msg + " " + I18nWeb(c, "success")
m.Msg = msg + I18nWeb(c, "success")
}
} else {
m.Success = false
m.Msg = msg + " " + I18nWeb(c, "fail") + ": " + err.Error()
logger.Warning(msg+" "+I18nWeb(c, "fail")+": ", err)
m.Msg = msg + I18nWeb(c, "fail") + ": " + err.Error()
logger.Warning(msg+I18nWeb(c, "fail")+": ", err)
}
c.JSON(http.StatusOK, m)
}

View File

@@ -30,7 +30,6 @@ type AllSetting struct {
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"`
TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"`
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
@@ -40,7 +39,6 @@ type AllSetting struct {
TimeLocation string `json:"timeLocation" form:"timeLocation"`
SecretEnable bool `json:"secretEnable" form:"secretEnable"`
SubEnable bool `json:"subEnable" form:"subEnable"`
SubSyncEnable bool `json:"subSyncEnable" form:"subSyncEnable"`
SubListen string `json:"subListen" form:"subListen"`
SubPort int `json:"subPort" form:"subPort"`
SubPath string `json:"subPath" form:"subPath"`
@@ -54,7 +52,7 @@ type AllSetting struct {
SubJsonPath string `json:"subJsonPath" form:"subJsonPath"`
SubJsonURI string `json:"subJsonURI" form:"subJsonURI"`
SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"`
SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"`
SubJsonNoise string `json:"subJsonNoise" form:"subJsonNoise"`
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"`
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
Datepicker string `json:"datepicker" form:"datepicker"`

View File

@@ -23,15 +23,13 @@
</tr-qr-bg>
</tr-qr-box>
</template>
<template v-if="!isJustSub">
<template v-for="(row, index) in qrModal.qrcodes">
<tr-qr-box class="qr-box">
<a-tag color="green" class="qr-tag"><span>[[ row.remark ]]</span></a-tag>
<tr-qr-bg class="qr-bg">
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas>
</tr-qr-bg>
</tr-qr-box>
</template>
<template v-for="(row, index) in qrModal.qrcodes">
<tr-qr-box class="qr-box">
<a-tag color="green" class="qr-tag"><span>[[ row.remark ]]</span></a-tag>
<tr-qr-bg class="qr-bg">
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas>
</tr-qr-bg>
</tr-qr-box>
</template>
</tr-qr-modal>
</a-modal>
@@ -45,14 +43,12 @@
qrcodes: [],
clipboard: null,
visible: false,
isJustSub: false,
subId: '',
show: function(title = '', dbInbound, client, isJustSub = false) {
show: function(title = '', dbInbound, client) {
this.title = title;
this.dbInbound = dbInbound;
this.inbound = dbInbound.toInbound();
this.client = client;
this.isJustSub = isJustSub;
this.subId = '';
this.qrcodes = [];
if (this.inbound.protocol == Protocols.WIREGUARD) {
@@ -80,9 +76,7 @@
delimiters: ['[[', ']]'],
el: '#qrcode-modal',
data: {
qrModal: qrModal,get isJustSub(){
return qrModal.isJustSub
}
qrModal: qrModal,
},
methods: {
copyToClipboard(elementId, content) {

View File

@@ -449,7 +449,7 @@
<a-row justify="center" class="centered">
<a-col :span="24">
<a-select ref="selectLang" v-model="lang"
@change="setLang(lang)" style="width: 200px;"
@change="setLang(lang)" style="width: 150px;"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l.value" label="English" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span>

View File

@@ -14,10 +14,10 @@
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "pages.client.first" }}' v-if="clientsBulkModal.emailMethod>1">
<a-input-number v-model.number="clientsBulkModal.firstNum" :min="1"></a-input-number>
<a-input-number v-model="clientsBulkModal.firstNum" :min="1"></a-input-number>
</a-form-item>
<a-form-item label='{{ i18n "pages.client.last" }}' v-if="clientsBulkModal.emailMethod>1">
<a-input-number v-model.number="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
<a-input-number v-model="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
</a-form-item>
<a-form-item label='{{ i18n "pages.client.prefix" }}' v-if="clientsBulkModal.emailMethod>0">
<a-input v-model.trim="clientsBulkModal.emailPrefix"></a-input>
@@ -26,9 +26,9 @@
<a-input v-model.trim="clientsBulkModal.emailPostfix"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.client.clientCount" }}' v-if="clientsBulkModal.emailMethod < 2">
<a-input-number v-model.number="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
</a-form-item>
<a-form-item label='{{ i18n "security" }}' v-if="inbound.protocol === Protocols.VMESS">
<a-form-item v-if="inbound.protocol === Protocols.VMESS" label='Security'>
<a-select v-model="clientsBulkModal.security" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
</a-select>
@@ -39,6 +39,12 @@
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Flow' v-if="clientsBulkModal.inbound.xtls">
<a-select v-model="clientsBulkModal.flow" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="app.subSettings.enable">
<template slot="label">
<a-tooltip>
@@ -61,7 +67,7 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number style="width: 50%" v-model.number="clientsBulkModal.tgId" min="0"></a-input-number>
<a-input-number style="width: 50%" v-model="clientsBulkModal.tgId" min="0"></a-input-number>
</a-form-item>
<a-form-item v-if="app.ipLimitEnable">
<template slot="label">
@@ -73,7 +79,7 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number v-model.number="clientsBulkModal.limitIp" min="0"></a-input-number>
<a-input-number v-model="clientsBulkModal.limitIp" min="0"></a-input-number>
</a-form-item>
<a-form-item>
<template slot="label">
@@ -85,7 +91,7 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number v-model.number="clientsBulkModal.totalGB" :min="0"></a-input-number>
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
@@ -175,6 +181,9 @@
if (clientsBulkModal.inbound.canEnableTlsFlow()) {
newClient.flow = clientsBulkModal.flow;
}
if (clientsBulkModal.inbound.xtls) {
newClient.flow = clientsBulkModal.flow;
}
newClient.reset = clientsBulkModal.reset;
clients.push(newClient);
}

View File

@@ -16,16 +16,7 @@
title: '',
okText: '',
isEdit: false,
group: {
canGroup: true,
isGroup: false,
currentClient: null,
inbounds: [],
clients: [],
editIds: []
},
dbInbound: new DBInbound(),
dbInbounds: null,
inbound: new Inbound(),
clients: [],
clientStats: [],
@@ -34,126 +25,33 @@
clientIps: null,
delayedStart: false,
ok() {
if (app.subSettings.enable && clientModal.group.isGroup && clientModal.group.canGroup) {
const currentClient = clientModal.group.currentClient;
const { limitIp, comment, totalGB, expiryTime, reset, enable, subId, tgId, flow } = currentClient;
const uniqueEmails = clientModalApp.makeGroupEmailsUnique(clientModal.dbInbounds, currentClient.email, clientModal.group.clients);
clientModal.group.clients.forEach((client, index) => {
client.email = uniqueEmails[index];
client.limitIp = limitIp;
client.comment = comment;
client.totalGB = totalGB;
client.expiryTime = expiryTime;
client.reset = reset;
client.enable = enable;
if (subId) {
client.subId = subId;
}
if (tgId) {
client.tgId = tgId;
}
if (flow) {
client.flow = flow;
}
});
if (clientModal.isEdit) {
ObjectUtil.execute(clientModal.confirm, clientModal.group.clients, clientModal.group.inbounds, clientModal.group.editIds);
} else {
ObjectUtil.execute(clientModal.confirm, clientModal.group.clients, clientModal.group.inbounds);
}
if (clientModal.isEdit) {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);
} else {
if (clientModal.isEdit) {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);
} else {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
}
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
}
},
show({
title = '',
okText = '{{ i18n "sure" }}',
index = null,
dbInbound = null,
dbInbounds = null,
confirm = () => {
},
isEdit = false
}) {
this.group = {
canGroup: true,
isGroup: false,
currentClient: null,
inbounds: [],
clients: [],
editIds: []
}
this.dbInbounds = dbInbounds;
show({ title = '', okText = '{{ i18n "sure" }}', index = null, dbInbound = null, confirm = () => { }, isEdit = false }) {
this.visible = true;
this.title = title;
this.okText = okText;
this.isEdit = isEdit;
if (app.subSettings.enable && dbInbounds !== null && Array.isArray(dbInbounds)) {
if (isEdit) {
this.showProcess(dbInbound, index);
let processSingleEdit = true
if (this.group.canGroup) {
this.group.currentClient = this.clients[this.index]
const response = app.getSubGroupClients(dbInbounds, this.group.currentClient)
if (response.clients.length > 1) {
this.group.isGroup = true;
this.group.inbounds = response.inbounds
this.group.clients = response.clients
this.group.editIds = response.editIds
if (this.clients[index].expiryTime < 0) {
this.delayedStart = true;
}
processSingleEdit = false
}
}
if (processSingleEdit) {
this.singleEditClientProcess(index)
}
} else {
this.group.isGroup = true;
dbInbounds.forEach((dbInboundItem) => {
this.showProcess(dbInboundItem);
if (this.dbInbound.isMultiUser()) {
this.addClient(this.inbound.protocol, this.clients);
this.group.inbounds.push(dbInboundItem.id)
this.group.clients.push(this.clients[this.index])
}
})
this.group.currentClient = this.clients[this.index]
this.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound();
this.clients = this.inbound.clients;
this.index = index === null ? this.clients.length : index;
this.delayedStart = false;
if (isEdit) {
if (this.clients[index].expiryTime < 0) {
this.delayedStart = true;
}
this.oldClientId = this.getClientId(dbInbound.protocol, clients[index]);
} else {
this.showProcess(dbInbound, index);
if (isEdit) {
this.singleEditClientProcess(index)
} else {
this.addClient(this.inbound.protocol, this.clients);
}
this.addClient(this.inbound.protocol, this.clients);
}
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
this.confirm = confirm;
},
showProcess(dbInbound, index = null) {
this.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound();
if (this.dbInbound.isMultiUser()) {
this.clients = this.inbound.clients;
this.index = index === null ? this.clients.length : index;
this.delayedStart = false;
}
},
singleEditClientProcess(index) {
if (this.clients[index].expiryTime < 0) {
this.delayedStart = true;
}
this.oldClientId = this.getClientId(this.dbInbound.protocol, this.clients[index]);
},
},
getClientId(protocol, client) {
switch (protocol) {
case Protocols.TROJAN: return client.password;
@@ -174,7 +72,7 @@
clientModal.visible = false;
clientModal.loading(false);
},
loading(loading = true) {
loading(loading=true) {
clientModal.confirmLoading = loading;
},
};
@@ -196,18 +94,6 @@
get isEdit() {
return this.clientModal.isEdit;
},
get isGroup() {
return this.clientModal.group.isGroup;
},
get isGroupEdit() {
return this.clientModal.group.canGroup;
},
set isGroupEdit(value) {
this.clientModal.group.canGroup = value;
if (!value) {
this.clientModal.singleEditClientProcess(this.clientModal.index)
}
},
get datepicker() {
return app.datepicker;
},
@@ -234,36 +120,6 @@
},
},
methods: {
makeGroupEmailsUnique(dbInbounds, baseEmail, groupClients) {
// Extract the base part of the email (before the "__" if present)
const match = baseEmail.match(/^(.*?)__/);
const base = match ? match[1] : baseEmail;
// Generate initial emails for each client in the group
const generatedEmails = groupClients.map((_, index) => `${base}__${index + 1}`);
// Function to check if an email already exists in dbInbounds but belongs to a different subId
const isDuplicate = (emailToCheck, clientSubId) => {
return dbInbounds.some((dbInbound) => {
const settings = JSON.parse(dbInbound.settings);
const clients = settings && settings.clients ? settings.clients : [];
return clients.some(client => client.email === emailToCheck && client.subId !== clientSubId);
});
};
// Check if any of the generated emails are duplicates
const hasDuplicates = generatedEmails.some((email, index) => {
return isDuplicate(email, groupClients[index].subId);
});
// If duplicates exist, add a random string to the base email to ensure uniqueness
if (hasDuplicates) {
const randomString = `-${RandomUtil.randomLowerAndNum(4)}`;
return groupClients.map((_, index) => `${base}${randomString}__${index + 1}`);
}
return generatedEmails;
},
async getDBClientIps(email) {
const msg = await HttpUtil.post(`/panel/inbound/clientIps/${email}`);
if (!msg.success) {
@@ -291,22 +147,7 @@
} catch (error) {
}
},
async resetClientTrafficHandler(client, dbInboundId, clients = []) {
if (clients.length > 0) {
const resetRequests = clients
.filter(client => {
const inbound = clientModal.dbInbounds.find(inbound => inbound.id === client.inboundId);
return inbound && app.hasClientStats(inbound, client.email);
}).map(client => ({ inboundId: client.inboundId, email: client.email}));
return HttpUtil.postWithModalJson('/panel/inbound/resetGroupClientTraffic', resetRequests, null)
} else {
return HttpUtil.postWithModal('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email)
}
},
resetClientTraffic(client, dbInboundId, iconElement) {
const subGroup = app.subSettings.enable && clientModal.group.isGroup && clientModal.group.canGroup && clientModal.dbInbounds && clientModal.dbInbounds.length > 0 ? app.getSubGroupClients(clientModal.dbInbounds, client) : [];
const clients = subGroup && subGroup.clients && subGroup.clients.length > 1 ? subGroup.clients : [];
resetClientTraffic(email, dbInboundId, iconElement) {
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
@@ -315,8 +156,8 @@
cancelText: '{{ i18n "cancel"}}',
onOk: async () => {
iconElement.disabled = true;
const msg = await this.resetClientTrafficHandler(client, dbInboundId, clients);
if (msg && msg.success) {
const msg = await HttpUtil.postWithModal('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + email);
if (msg.success) {
this.clientModal.clientStats.up = 0;
this.clientModal.clientStats.down = 0;
}

View File

@@ -1,7 +1,5 @@
{{define "dnsModal"}}
<a-modal id="dns-modal" v-model="dnsModal.visible" :title="dnsModal.title" @ok="dnsModal.ok" :closable="true"
:mask-closable="false" :ok-text="dnsModal.okText" cancel-text='{{ i18n "close" }}'
:class="themeSwitcher.currentTheme">
<a-modal id="dns-modal" v-model="dnsModal.visible" :title="dnsModal.title" @ok="dnsModal.ok" :closable="true" :mask-closable="false" :ok-text="dnsModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.xray.outbound.address" }}'>
<a-input v-model.trim="dnsModal.dnsServer.address"></a-input>
@@ -10,29 +8,18 @@
<a-button icon="plus" size="small" type="primary" @click="dnsModal.dnsServer.domains.push('')"></a-button>
<template v-for="(domain, index) in dnsModal.dnsServer.domains">
<a-input v-model.trim="dnsModal.dnsServer.domains[index]">
<a-button icon="minus" size="small" slot="addonAfter"
@click="dnsModal.dnsServer.domains.splice(index,1)"></a-button>
<a-button icon="minus" size="small" slot="addonAfter" @click="dnsModal.dnsServer.domains.splice(index,1)"></a-button>
</a-input>
</template>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.dns.strategy" }}' v-if="isAdvanced">
<a-select v-model="dnsModal.dnsServer.queryStrategy" style="width: 100%"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="dnsModal.dnsServer.queryStrategy" style="width: 100%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l" :label="l" v-for="l in ['UseIP', 'UseIPv4', 'UseIPv6']"> [[ l ]] </a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Skip Fallback' v-if="isAdvanced">
<a-switch v-model="dnsModal.dnsServer.skipFallback"></a-switch>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.dns.expectIPs"}}'>
<a-button icon="plus" size="small" type="primary" @click="dnsModal.dnsServer.expectIPs.push('')"></a-button>
<template v-for="(domain, index) in dnsModal.dnsServer.expectIPs">
<a-input v-model.trim="dnsModal.dnsServer.expectIPs[index]">
<a-button icon="minus" size="small" slot="addonAfter"
@click="dnsModal.dnsServer.expectIPs.splice(index,1)"></a-button>
</a-input>
</template>
</a-form-item>
</a-form>
</a-modal>
<script>
@@ -45,24 +32,20 @@
dnsServer: {
address: "localhost",
domains: [],
expectIPs: [],
queryStrategy: 'UseIP',
skipFallback: true,
},
ok() {
domains = dnsModal.dnsServer.domains.filter(d => d.length > 0);
expectIPs = dnsModal.dnsServer.expectIPs.filter(ip => ip.length > 0);
dnsModal.dnsServer.domains = domains;
dnsModal.dnsServer.expectIPs = expectIPs;
newDnsServer = (domains.length > 0 || expectIPs.length > 0) ? dnsModal.dnsServer : dnsModal.dnsServer.address;
newDnsServer = domains.length > 0 ? dnsModal.dnsServer : dnsModal.dnsServer.address;
ObjectUtil.execute(dnsModal.confirm, newDnsServer);
},
show({
title = '',
okText = '{{ i18n "confirm" }}',
dnsServer,
confirm = (dnsServer) => { },
confirm = (dnsServer) => {},
isEdit = false
}) {
this.title = title;
@@ -76,7 +59,6 @@
this.dnsServer = {
address: dnsServer ?? "",
domains: [],
expectIPs: [],
queryStrategy: 'UseIP',
skipFallback: true,
}
@@ -85,7 +67,6 @@
this.dnsServer = {
address: "localhost",
domains: [],
expectIPs: [],
queryStrategy: 'UseIP',
skipFallback: true,
}
@@ -104,8 +85,8 @@
},
computed: {
isAdvanced: {
get: function () {
return dnsModal.dnsServer.domains.length > 0;
get: function() {
return dnsModal.dnsServer.domains.length > 0
}
}
}

View File

@@ -7,7 +7,7 @@
<a-input v-model.trim="fakednsModal.fakeDns.ipPool"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.fakedns.poolSize" }}'>
<a-input-number v-model.number="fakednsModal.fakeDns.poolSize" :min="1"></a-input-number>
<a-input-number style="width: 100%;" type="number" min="1" v-model.trim="fakednsModal.fakeDns.poolSize"></a-input-number>
</a-form-item>
</a-form>
</a-modal>

View File

@@ -1,15 +0,0 @@
{{define "form/allocate"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='Strategy'>
<a-select v-model="inbound.allocate.strategy" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['always','random']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Refresh'>
<a-input-number v-model.number="inbound.allocate.refresh" min="0"></a-input-number>
</a-form-item>
<a-form-item label='Concurrency'>
<a-input-number v-model.number="inbound.allocate.concurrency" min="0"></a-input-number>
</a-form-item>
</a-form>
{{end}}

View File

@@ -3,18 +3,6 @@
<a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
<a-switch v-model="client.enable"></a-switch>
</a-form-item>
<a-form-item v-if="isEdit && app.subSettings.enable && isGroup">
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.client.isGroupEditDesc" }}</span>
</template>
{{ i18n "pages.client.isGroupEdit" }}
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-switch v-model="isGroupEdit"></a-switch>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
@@ -51,7 +39,7 @@
</template>
<a-input v-model.trim="client.id"></a-input>
</a-form-item>
<a-form-item v-if="inbound.protocol === Protocols.VMESS" label='{{ i18n "security" }}'>
<a-form-item v-if="inbound.protocol === Protocols.VMESS" label='Security'>
<a-select v-model="client.security" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
</a-select>
@@ -78,10 +66,7 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number style="width: 50%" v-model.number="client.tgId" min="0"></a-input-number>
</a-form-item>
<a-form-item v-if="client.email" label='{{ i18n "comment" }}'>
<a-input v-model.trim="client.comment"></a-input>
<a-input-number style="width: 50%" v-model="client.tgId" min="0"></a-input-number>
</a-form-item>
<a-form-item v-if="app.ipLimitEnable">
<template slot="label">
@@ -93,7 +78,7 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number v-model.number="client.limitIp" min="0"></a-input-number>
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
</a-form-item>
<a-form-item v-if="app.ipLimitEnable && client.limitIp > 0 && client.email && isEdit">
<template slot="label">
@@ -119,6 +104,12 @@
</a-textarea>
</a-form>
</a-form-item>
<a-form-item v-if="inbound.xtls" label='Flow'>
<a-select v-model="client.flow" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="inbound.canEnableTlsFlow()" label='Flow'>
<a-select v-model="client.flow" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
@@ -135,7 +126,7 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number v-model.number="client._totalGB" :min="0"></a-input-number>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-if="isEdit && clientStats" label='{{ i18n "usage" }}'>
<a-tag :color="clientUsageColor(clientStats, app.trafficDiff)">
@@ -146,7 +137,7 @@
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon type="retweet"
@click="resetClientTraffic(client,clientStats.inboundId,$event.target)"
@click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)"
v-if="client.email.length > 0"></a-icon>
</a-tooltip>
</a-form-item>

View File

@@ -41,7 +41,7 @@
<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="dbInbound.totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item>
@@ -115,19 +115,7 @@
</template>
<!-- sniffing -->
<a-collapse>
<a-collapse-panel header='Sniffing'>
{{template "form/sniffing"}}
</a-collapse-panel>
</a-collapse>
<!-- allocate -->
<!-- Temporarily disabled until we accepts range for port allocation
<a-collapse>
<a-collapse-panel header='Allocate'>
{{template "form/allocate"}}
</a-collapse-panel>
</a-collapse>
-->
<template>
{{template "form/sniffing"}}
</template>
{{end}}

View File

@@ -22,6 +22,9 @@
<a-select-option v-for="s in OutboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Timeout'>
<a-input-number v-model.number="outbound.settings.timeout" min="0" ></a-input-number>
</a-form-item>
<a-form-item label='Redirect'>
<a-input v-model="outbound.settings.redirect"></a-input>
</a-form-item>
@@ -43,39 +46,16 @@
<a-input v-model.trim="outbound.settings.fragment.interval"></a-input>
</a-form-item>
</template>
<!-- Switch for Noises -->
<a-form-item label='Noises'>
<a-switch :checked="outbound.settings.noises.length > 0"
@change="checked => outbound.settings.noises = checked ? [new Outbound.FreedomSettings.Noise()] : []">
</a-switch>
<a-form-item label='Noise'>
<a-switch :checked="Object.keys(outbound.settings.noise).length >0" @change="checked => outbound.settings.noise = checked ? new Outbound.FreedomSettings.Noise() : {}"></a-switch>
</a-form-item>
<!-- 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>
<template v-if="Object.keys(outbound.settings.noise).length >0">
<a-form-item label='Packet'>
<a-input v-model.trim="outbound.settings.noise.packet"></a-input>
</a-form-item>
<a-form-item label='Delay'>
<a-input v-model.trim="outbound.settings.noise.delay"></a-input>
</a-form-item>
<!-- Noise Configurations -->
<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" @click="() => outbound.settings.delNoise(index)"
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']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Packet'>
<a-input v-model.trim="noise.packet"></a-input>
</a-form-item>
<a-form-item label='Delay'>
<a-input v-model.trim="noise.delay"></a-input>
</a-form-item>
</a-form>
</template>
</template>
@@ -95,14 +75,6 @@
<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 ['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-input v-model.number="outbound.settings.blockTypes"></a-input>
</a-form-item>
</template>
<!-- wireguard settings -->
@@ -147,8 +119,8 @@
<a-form-item label='Workers'>
<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>
<a-form-item label='Kernel Mode'>
<a-switch v-model="outbound.settings.kernelMode"></a-switch>
</a-form-item>
<a-form-item>
<template slot="label">
@@ -207,15 +179,11 @@
<a-form-item label='ID'>
<a-input v-model.trim="outbound.settings.id"></a-input>
</a-form-item>
<!-- 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>
</a-form-item>
</template>
<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>
</a-form-item>
<!-- vless settings -->
<template v-if="outbound.canEnableTlsFlow()">
@@ -267,12 +235,14 @@
<template v-if="outbound.canEnableStream()">
<a-form-item label='{{ i18n "transmission" }}'>
<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="tcp">TCP</a-select-option>
<a-select-option value="kcp">mKCP</a-select-option>
<a-select-option value="ws">WebSocket</a-select-option>
<a-select-option value="http">H2</a-select-option>
<a-select-option value="quic">QUIC</a-select-option>
<a-select-option value="grpc">gRPC</a-select-option>
<a-select-option value="httpupgrade">HTTPUpgrade</a-select-option>
<a-select-option value="xhttp">XHTTP</a-select-option>
<a-select-option value="splithttp">SplitHTTP</a-select-option>
</a-select>
</a-form-item>
<template v-if="outbound.stream.network === 'tcp'">
@@ -336,11 +306,42 @@
<a-form-item label='{{ i18n "path" }}'>
<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>
</template>
<!-- http -->
<template v-if="outbound.stream.network === 'http'">
<a-form-item label='{{ i18n "host" }}'>
<a-input v-model.trim="outbound.stream.http.host"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="outbound.stream.http.path"></a-input>
</a-form-item>
</template>
<!-- quic -->
<template v-if="outbound.stream.network === 'quic'">
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
<a-select v-model="outbound.stream.quic.security" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">None</a-select-option>
<a-select-option value="aes-128-gcm">AES-128-GCM</a-select-option>
<a-select-option value="chacha20-poly1305">CHACHA20-POLY1305</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="outbound.stream.quic.key"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="outbound.stream.quic.type" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">None</a-select-option>
<a-select-option value="srtp">SRTP</a-select-option>
<a-select-option value="utp">uTP</a-select-option>
<a-select-option value="wechat-video">WeChat</a-select-option>
<a-select-option value="dtls">DTLS 1.2</a-select-option>
<a-select-option value="wireguard">WireGuard</a-select-option>
</a-select>
</a-form-item>
</template>
<!-- grpc -->
<template v-if="outbound.stream.network === 'grpc'">
<a-form-item label='Service Name'>
@@ -364,42 +365,13 @@
</a-form-item>
</template>
<!-- xhttp -->
<template v-if="outbound.stream.network === 'xhttp'">
<!-- splithttp -->
<template v-if="outbound.stream.network === 'splithttp'">
<a-form-item label='{{ i18n "host" }}'>
<a-input v-model="outbound.stream.xhttp.host"></a-input>
<a-input v-model="outbound.stream.splithttp.host"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "path" }}'>
<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>
</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>
<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>
<a-form-item label="Max Reuse Times">
<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-form-item>
<a-form-item label="Max Reusable Secs">
<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 v-model.trim="outbound.stream.splithttp.path"></a-input>
</a-form-item>
</template>
</template>
@@ -469,13 +441,13 @@
<a-switch v-model="outbound.stream.sockopt.tcpFastOpen"></a-switch>
</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="outbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="Multipath TCP">
<a-switch v-model.trim="outbound.stream.sockopt.tcpMptcp"></a-switch>
</a-form-item>
<a-form-item label="Penetrate">
<a-switch v-model="outbound.stream.sockopt.penetrate"></a-switch>
</a-form-item>
<a-form-item label="TCP No-Delay">
<a-switch v-model="outbound.stream.sockopt.tcpNoDelay"></a-switch>
</a-form-item>
</template>
@@ -486,10 +458,10 @@
</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="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="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">

View File

@@ -16,5 +16,8 @@
<a-form-item label='Follow Redirect'>
<a-switch v-model="inbound.settings.followRedirect"></a-switch>
</a-form-item>
<a-form-item label='Timeout'>
<a-input-number v-model.number="inbound.settings.timeout" :min="0"></a-input-number>
</a-form-item>
</a-form>
{{end}}

View File

@@ -19,8 +19,5 @@
</template>
</a-input>
</a-input-group>
<a-form-item label="Allow Transparent">
<a-switch v-model="inbound.settings.allowTransparent" />
</a-form-item>
</a-form>
{{end}}

View File

@@ -43,8 +43,5 @@
<a-select-option value="udp">UDP</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='ivCheck'>
<a-switch v-model="inbound.settings.ivCheck"></a-switch>
</a-form-item>
</a-form>
{{end}}

View File

@@ -18,7 +18,7 @@
</table>
</a-collapse-panel>
</a-collapse>
<template v-if="inbound.isTcp">
<template v-if="inbound.isTcp && !inbound.stream.isReality">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks">
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
@@ -42,7 +42,7 @@
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label='xVer'>
<a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
<a-input-number v-model="fallback.xver" :min="0" :max="2"></a-input-number>
</a-form-item>
</a-form>
<a-divider style="margin:5px 0;"></a-divider>

View File

@@ -18,7 +18,7 @@
</table>
</a-collapse-panel>
</a-collapse>
<template v-if="inbound.isTcp">
<template v-if="inbound.isTcp && !inbound.stream.isReality">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks">
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
@@ -42,7 +42,7 @@
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label='xVer'>
<a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
<a-input-number v-model="fallback.xver" :min="0" :max="2"></a-input-number>
</a-form-item>
</a-form>
<a-divider style="margin:5px 0;"></a-divider>

View File

@@ -10,7 +10,7 @@
<tr class="client-table-header">
<th>{{ i18n "pages.inbounds.email" }}</th>
<th>ID</th>
<th>{{ i18n "security" }}</th>
<th>Security</th>
</tr>
<tr v-for="(client, index) in inbound.settings.vmesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td>[[ client.email ]]</td>

View File

@@ -18,8 +18,8 @@
<a-form-item label='MTU'>
<a-input-number v-model.number="inbound.settings.mtu"></a-input-number>
</a-form-item>
<a-form-item label='No Kernel Tun'>
<a-switch v-model="inbound.settings.noKernelTun"></a-switch>
<a-form-item label='Kernel Mode'>
<a-switch v-model="inbound.settings.kernelMode"></a-switch>
</a-form-item>
<a-form-item label="Peers">
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addPeer()"></a-button>

View File

@@ -1,56 +0,0 @@
{{define "form/realitySettings"}}
<template>
<a-form-item label='Show'>
<a-switch v-model="inbound.stream.reality.show"></a-switch>
</a-form-item>
<a-form-item label='Xver'>
<a-input-number v-model.number="inbound.stream.reality.xver" :min="0"></a-input-number>
</a-form-item>
<a-form-item label='uTLS'>
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 50%"
: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='Dest (Target)'>
<a-input v-model.trim="inbound.stream.reality.dest"></a-input>
</a-form-item>
<a-form-item label='SNI'>
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
</a-form-item>
<a-form-item label='Max Time Diff (ms)'>
<a-input-number v-model.number="inbound.stream.reality.maxTimediff" :min="0"></a-input-number>
</a-form-item>
<!-- we also have this but i think it's not necessary
<a-form-item label='Min Client'>
<a-input v-model.trim="inbound.stream.reality.minClient"></a-input>
</a-form-item>
<a-form-item label='Max Client'>
<a-input v-model.trim="inbound.stream.reality.maxClient"></a-input>
</a-form-item>
-->
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template> Short IDs <a-icon @click="inbound.stream.reality.shortIds = RandomUtil.randomShortIds()"
type="sync"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="inbound.stream.reality.shortIds"></a-input>
</a-form-item>
<a-form-item label='SpiderX'>
<a-input v-model.trim="inbound.stream.reality.settings.spiderX"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-input v-model.trim="inbound.stream.reality.privateKey"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-input v-model.trim="inbound.stream.reality.settings.publicKey"></a-input>
</a-form-item>
<a-form-item label=" ">
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert</a-button>
</a-form-item>
</template>
{{end}}

View File

@@ -1,8 +1,9 @@
{{define "form/sniffing"}}
<a-divider style="margin:5px 0 0;"></a-divider>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item>
<span slot="label">
{{ i18n "enabled" }}
Sniffing
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span>

View File

@@ -0,0 +1,17 @@
{{define "form/streamHTTP"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="inbound.stream.http.path"></a-input>
</a-form-item>
<a-form-item>
<template slot="label">{{ i18n "host" }}
<a-button icon="plus" size="small" @click="inbound.stream.http.addHost()"></a-button>
</template>
<template v-for="(host, index) in inbound.stream.http.host">
<a-input v-model.trim="inbound.stream.http.host[index]">
<a-button icon="minus" size="small" slot="addonAfter" @click="inbound.stream.http.removeHost(index)" v-if="inbound.stream.http.host.length>1"></a-button>
</a-input>
</template>
</a-form-item>
</a-form>
{{end}}

View File

@@ -10,7 +10,7 @@
<a-input v-model.trim="inbound.stream.httpupgrade.path"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button icon="plus" size="small" @click="inbound.stream.httpupgrade.addHeader('', '')"></a-button>
<a-button icon="plus" size="small" @click="inbound.stream.httpupgrade.addHeader('host', '')"></a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.httpupgrade.headers">

View File

@@ -0,0 +1,33 @@
{{define "form/streamQUIC"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
<a-select v-model="inbound.stream.quic.security" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">None</a-select-option>
<a-select-option value="aes-128-gcm">AES-128-GCM</a-select-option>
<a-select-option value="chacha20-poly1305">CHACHA20-POLY1305</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template>
{{ i18n "password" }}
<a-icon @click="inbound.stream.quic.key = RandomUtil.randomSeq(10)"type="sync"> </a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="inbound.stream.quic.key"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="inbound.stream.quic.type" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">None</a-select-option>
<a-select-option value="srtp">SRTP</a-select-option>
<a-select-option value="utp">uTP</a-select-option>
<a-select-option value="wechat-video">WeChat</a-select-option>
<a-select-option value="dtls">DTLS 1.2</a-select-option>
<a-select-option value="wireguard">WireGuard</a-select-option>
</a-select>
</a-form-item>
</a-form>
{{end}}

View File

@@ -4,12 +4,14 @@
<a-form-item label='{{ i18n "transmission" }}'>
<a-select v-model="inbound.stream.network" style="width: 75%" @change="streamNetworkChange"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tcp">TCP (RAW)</a-select-option>
<a-select-option value="tcp">TCP</a-select-option>
<a-select-option value="kcp">mKCP</a-select-option>
<a-select-option value="ws">WebSocket</a-select-option>
<a-select-option value="http">H2</a-select-option>
<a-select-option value="quic">QUIC</a-select-option>
<a-select-option value="grpc">gRPC</a-select-option>
<a-select-option value="httpupgrade">HTTPUpgrade</a-select-option>
<a-select-option value="xhttp">XHTTP</a-select-option>
<a-select-option value="splithttp">SplitHTTP</a-select-option>
</a-select>
</a-form-item>
</a-form>
@@ -29,6 +31,16 @@
{{template "form/streamWS"}}
</template>
<!-- http -->
<template v-if="inbound.stream.network === 'http'">
{{template "form/streamHTTP"}}
</template>
<!-- quic -->
<template v-if="inbound.stream.network === 'quic'">
{{template "form/streamQUIC"}}
</template>
<!-- grpc -->
<template v-if="inbound.stream.network === 'grpc'">
{{template "form/streamGRPC"}}
@@ -39,9 +51,9 @@
{{template "form/streamHTTPUpgrade"}}
</template>
<!-- xhttp -->
<template v-if="inbound.stream.network === 'xhttp'">
{{template "form/streamXHTTP"}}
<!-- splithttp -->
<template v-if="inbound.stream.network === 'splithttp'">
{{template "form/streamSplitHTTP"}}
</template>
<!-- sockopt -->

View File

@@ -6,22 +6,22 @@
</a-form-item>
<template v-if="inbound.stream.sockoptSwitch">
<a-form-item label="Route Mark">
<a-input-number v-model.number="inbound.stream.sockopt.mark" :min="0"></a-input-number>
<a-input-number v-model="inbound.stream.sockopt.mark" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="TCP Keep Alive Interval">
<a-input-number v-model.number="inbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number>
<a-input-number v-model="inbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="TCP Keep Alive Idle">
<a-input-number v-model.number="inbound.stream.sockopt.tcpKeepAliveIdle" :min="0"></a-input-number>
<a-input-number v-model="inbound.stream.sockopt.tcpKeepAliveIdle" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="TCP Max Seg">
<a-input-number v-model.number="inbound.stream.sockopt.tcpMaxSeg" :min="0"></a-input-number>
<a-input-number v-model="inbound.stream.sockopt.tcpMaxSeg" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="TCP User Timeout">
<a-input-number v-model.number="inbound.stream.sockopt.tcpUserTimeout" :min="0"></a-input-number>
<a-input-number v-model="inbound.stream.sockopt.tcpUserTimeout" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="TCP Window Clamp">
<a-input-number v-model.number="inbound.stream.sockopt.tcpWindowClamp" :min="0"></a-input-number>
<a-input-number v-model="inbound.stream.sockopt.tcpWindowClamp" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="Proxy Protocol">
<a-switch v-model="inbound.stream.sockopt.acceptProxyProtocol"></a-switch>
@@ -32,8 +32,8 @@
<a-form-item label="Multipath TCP">
<a-switch v-model.trim="inbound.stream.sockopt.tcpMptcp"></a-switch>
</a-form-item>
<a-form-item label="Penetrate">
<a-switch v-model.trim="inbound.stream.sockopt.penetrate"></a-switch>
<a-form-item label="TCP No-Delay">
<a-switch v-model.trim="inbound.stream.sockopt.tcpNoDelay"></a-switch>
</a-form-item>
<a-form-item label="V6 Only">
<a-switch v-model.trim="inbound.stream.sockopt.V6Only"></a-switch>

View File

@@ -0,0 +1,38 @@
{{define "form/streamSplitHTTP"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "host" }}'>
<a-input v-model.trim="inbound.stream.splithttp.host"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="inbound.stream.splithttp.path"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button icon="plus" size="small" @click="inbound.stream.splithttp.addHeader('host', '')"></a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.splithttp.headers">
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button slot="addonAfter" size="small" @click="inbound.stream.splithttp.removeHeader(index)">-</a-button>
</a-input>
</a-input-group>
</a-form-item>
<a-form-item label="Max Concurrent Upload">
<a-input v-model.trim="inbound.stream.splithttp.scMaxConcurrentPosts"></a-input>
</a-form-item>
<a-form-item label="Max Upload Size (Byte)">
<a-input v-model.trim="inbound.stream.splithttp.scMaxEachPostBytes"></a-input>
</a-form-item>
<a-form-item label="Min Upload Interval (Ms)">
<a-input v-model.trim="inbound.stream.splithttp.scMinPostsIntervalMs"></a-input>
</a-form-item>
<a-form-item label="Padding Bytes">
<a-input v-model.trim="inbound.stream.splithttp.xPaddingBytes"></a-input>
</a-form-item>
<a-form-item label="No SSE Header">
<a-switch v-model="inbound.stream.splithttp.noSSEHeader"></a-switch>
</a-form-item>
</a-form>
{{end}}

View File

@@ -9,11 +9,8 @@
<a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
</a-form-item>
<a-form-item label='Heartbeat Period'>
<a-input-number v-model.number="inbound.stream.ws.heartbeatPeriod" :min="0"></a-input-number>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button icon="plus" size="small" @click="inbound.stream.ws.addHeader('', '')"></a-button>
<a-button icon="plus" size="small" @click="inbound.stream.ws.addHeader('host', '')"></a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.ws.headers">

View File

@@ -1,46 +0,0 @@
{{define "form/streamXHTTP"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "host" }}'>
<a-input v-model.trim="inbound.stream.xhttp.host"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="inbound.stream.xhttp.path"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button icon="plus" size="small" @click="inbound.stream.xhttp.addHeader('', '')"></a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.xhttp.headers">
<a-input style="width: 50%" v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.xhttp.removeHeader(index)"></a-button>
</a-input>
</a-input-group>
</a-form-item>
<a-form-item label='Mode'>
<a-select v-model="inbound.stream.xhttp.mode" style="width: 50%"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in MODE_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Max Buffered Upload" v-if="inbound.stream.xhttp.mode === 'packet-up'">
<a-input-number v-model.number="inbound.stream.xhttp.scMaxBufferedPosts"></a-input-number>
</a-form-item>
<a-form-item label="Max Upload Size (Byte)" v-if="inbound.stream.xhttp.mode === 'packet-up'">
<a-input v-model.trim="inbound.stream.xhttp.scMaxEachPostBytes"></a-input>
</a-form-item>
<a-form-item label="Stream-Up Server" v-if="inbound.stream.xhttp.mode === 'stream-up'">
<a-input v-model.trim="inbound.stream.xhttp.scStreamUpServerSecs"></a-input>
</a-form-item>
<a-form-item label="Padding Bytes">
<a-input v-model.trim="inbound.stream.xhttp.xPaddingBytes"></a-input>
</a-form-item>
<a-form-item label="No SSE Header">
<a-switch v-model="inbound.stream.xhttp.noSSEHeader"></a-switch>
</a-form-item>
</a-form>
{{end}}

View File

@@ -5,7 +5,18 @@
<a-form-item label='{{ i18n "security" }}'>
<a-radio-group v-model="inbound.stream.security" button-style="solid">
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
<a-radio-button v-if="inbound.canEnableReality()" value="reality">Reality</a-radio-button>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.xtlsDesc" }}</span>
</template>
<a-radio-button v-if="inbound.canEnableXtls()" value="xtls">XTLS</a-radio-button>
</a-tooltip>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.realityDesc" }}</span>
</template>
<a-radio-button v-if="inbound.canEnableReality()" value="reality">REALITY</a-radio-button>
</a-tooltip>
<a-radio-button value="tls">TLS</a-radio-button>
</a-radio-group>
</a-form-item>
@@ -23,19 +34,16 @@
</a-form-item>
<a-form-item label="Min/Max Version">
<a-input-group compact>
<a-select v-model="inbound.stream.tls.minVersion" style="width: 50%"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="inbound.stream.tls.minVersion" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 50%"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-input-group>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 50%"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 50%" :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>
@@ -57,19 +65,14 @@
<a-form-item label="Session Resumption">
<a-switch v-model="inbound.stream.tls.enableSessionResumption"></a-switch>
</a-form-item>
<a-form-item label="Server Name To Verify">
<a-input v-model.trim="inbound.stream.tls.serverNameToVerify"></a-input>
</a-form-item>
<template v-for="cert,index in inbound.stream.tls.certs">
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
<a-button icon="plus" v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()"
style="margin-left: 10px"></a-button>
<a-button icon="minus" v-if="inbound.stream.tls.certs.length>1" type="primary" size="small"
@click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px"></a-button>
<a-button icon="plus" v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()" style="margin-left: 10px"></a-button>
<a-button icon="minus" v-if="inbound.stream.tls.certs.length>1" type="primary" size="small" @click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px"></a-button>
</a-form-item>
<template v-if="cert.useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
@@ -79,8 +82,7 @@
<a-input v-model.trim="cert.keyFile"></a-input>
</a-form-item>
<a-form-item label=" ">
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">
{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</a-form-item>
</template>
<template v-else>
@@ -102,15 +104,108 @@
<a-select-option v-for="key in USAGE_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Build Chain" v-if="cert.usage === 'issue'">
<a-form-item label="Build Chain">
<a-switch v-model="cert.buildChain"></a-switch>
</a-form-item>
</template>
</template>
<!-- xtls settings -->
<template v-else-if="inbound.xtls">
<a-form-item label="SNI" placeholder="Server Name Indication">
<a-input v-model.trim="inbound.stream.xtls.sni"></a-input>
</a-form-item>
<a-form-item label="ALPN">
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" v-model="inbound.stream.xtls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Allow Insecure">
<a-switch v-model="inbound.stream.xtls.settings.allowInsecure"></a-switch>
</a-form-item>
<template v-for="cert,index in inbound.stream.xtls.certs">
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
<a-button icon="plus" v-if="index === 0" type="primary" size="small" @click="inbound.stream.xtls.addCert()" style="margin-left: 10px"></a-button>
<a-button icon="minus" v-if="inbound.stream.xtls.certs.length>1" type="primary" size="small" @click="inbound.stream.xtls.removeCert(index)" style="margin-left: 10px"></a-button>
</a-form-item>
<template v-if="cert.useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-input v-model.trim="cert.certFile"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-input v-model.trim="cert.keyFile"></a-input>
</a-form-item>
<a-form-item label=" ">
<a-button type="primary" icon="import" @click="setDefaultCertXtls(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</a-form-item>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-input type="textarea" :rows="3" v-model="cert.cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-input type="textarea" :rows="3" v-model="cert.key"></a-input>
</a-form-item>
</template>
<a-form-item label='OCSP stapling'>
<a-input-number v-model.number="cert.ocspStapling" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="One Time Loading">
<a-switch v-model="cert.oneTimeLoading"></a-switch>
</a-form-item>
<a-form-item label='Usage Option'>
<a-select v-model="cert.usage" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in USAGE_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</template>
</template>
<!-- reality settings -->
<template v-if="inbound.stream.isReality">
{{template "form/realitySettings"}}
<a-form-item label='Show'>
<a-switch v-model="inbound.stream.reality.show"></a-switch>
</a-form-item>
<a-form-item label='Xver'>
<a-input-number v-model.number="inbound.stream.reality.xver" :min="0"></a-input-number>
</a-form-item>
<a-form-item label='uTLS'>
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 50%" :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='Dest'>
<a-input v-model.trim="inbound.stream.reality.dest"></a-input>
</a-form-item>
<a-form-item label='SNI'>
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template> Short IDs <a-icon @click="inbound.stream.reality.shortIds = RandomUtil.randomShortIds()" type="sync"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="inbound.stream.reality.shortIds"></a-input>
</a-form-item>
<a-form-item label='SpiderX'>
<a-input v-model.trim="inbound.stream.reality.settings.spiderX"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-input v-model.trim="inbound.stream.reality.privateKey"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-input v-model.trim="inbound.stream.reality.settings.publicKey"></a-input>
</a-form-item>
<a-form-item label=" ">
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert</a-button>
</a-form-item>
</template>
</a-form>
{{end}}
{{end}}

View File

@@ -12,7 +12,7 @@
<template slot="title">{{ i18n "info" }}</template>
<a-icon style="font-size: 24px;" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon>
</a-tooltip>
<a-tooltip v-if="hasClientStats(record, client.email)">
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-popconfirm @confirm="resetClientTraffic(client,record.id,false)" title='{{ i18n "pages.inbounds.resetTrafficContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}' cancel-text='{{ i18n "cancel"}}'>
<a-icon slot="icon" type="question-circle-o" :style="themeSwitcher.isDarkTheme ? 'color: var(--color-primary-100)' : 'color: var(--color-primary-100)'"></a-icon>

View File

@@ -34,7 +34,7 @@
<a-tag color="green">[[ inbound.network ]]</a-tag>
</td>
</tr>
<template v-if="inbound.isTcp || inbound.isWs || inbound.isHttpupgrade || inbound.isXHTTP">
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2 || inbound.isHttpupgrade || inbound.isSplithttp">
<tr>
<td>{{ i18n "host" }}</td>
<td v-if="inbound.host">
@@ -58,11 +58,23 @@
</td>
</tr>
</template>
<template v-if="inbound.isXHTTP">
<template v-if="inbound.isQuic">
<tr>
<td>Mode</td>
<td>quic {{ i18n "encryption" }}</td>
<td>
<a-tag>[[ inbound.stream.xhttp.mode ]]</a-tag>
<a-tag>[[ inbound.quicSecurity ]]</a-tag>
</td>
</tr>
<tr>
<td>quic {{ i18n "password" }}</td>
<td>
<a-tag>[[ inbound.quicKey ]]</a-tag>
</td>
</tr>
<tr>
<td>quic {{ i18n "camouflage" }}</td>
<td>
<a-tag>[[ inbound.quicType ]]</a-tag>
</td>
</tr>
</template>
@@ -147,13 +159,16 @@
<a-tag>[[ infoModal.clientSettings.id ]]</a-tag>
</td>
</tr>
<tr v-if="dbInbound.isVMess">
<td>{{ i18n "security" }}</td>
<td>
<a-tag>[[ infoModal.clientSettings.security ]]</a-tag>
<tr v-if="infoModal.inbound.canEnableTlsFlow()">
<td>Flow</td>
<td v-if="infoModal.clientSettings.flow">
<a-tag>[[ infoModal.clientSettings.flow ]]</a-tag>
</td>
<td v-else>
<a-tag color="orange">{{ i18n "none" }}</a-tag>
</td>
</tr>
<tr v-if="infoModal.inbound.canEnableTlsFlow()">
<tr v-if="infoModal.inbound.xtls">
<td>Flow</td>
<td v-if="infoModal.clientSettings.flow">
<a-tag>[[ infoModal.clientSettings.flow ]]</a-tag>
@@ -185,33 +200,6 @@
<a-tag>↑ [[ sizeFormat(infoModal.clientStats.up) ]] / [[ sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
</td>
</tr>
<tr v-if="infoModal.clientSettings.comment">
<td>{{ i18n "comment" }}</td>
<td>
<a-tooltip :title="[[ infoModal.clientSettings.comment ]]">
<a-tag class="info-large-tag">[[ infoModal.clientSettings.comment ]]</a-tag>
</a-tooltip>
</td>
</tr>
<tr v-if="app.ipLimitEnable">
<td>{{ i18n "pages.inbounds.IPLimit" }}</td>
<td>
<a-tag>[[ infoModal.clientSettings.limitIp ]]</a-tag>
</td>
</tr>
<tr v-if="app.ipLimitEnable">
<td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
<td>
<a-tag>[[ infoModal.clientIps ]]</a-tag>
<a-icon type="sync" :spin="refreshing" @click="refreshIPs" style="margin: 0 5px;"></a-icon>
<a-tooltip :title="[[ dbInbound.address ]]">
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
</template>
<a-icon type="delete" @click="clearClientIps"></a-icon>
</a-tooltip>
</td>
</tr>
</table>
<table style="display: inline-table; margin-block: 10px; width: 100%; text-align: center;">
<tr>
@@ -396,8 +384,8 @@
<td>[[ inbound.settings.mtu ]]</td>
</tr>
<tr>
<td>No Kernel Tun</td>
<td>[[ inbound.settings.noKernelTun ]]</td>
<td>Kernel Mode</td>
<td>[[ inbound.settings.kernelMode ]]</td>
</tr>
<template v-for="(peer, index) in inbound.settings.peers">
<tr>
@@ -427,7 +415,7 @@
</tr>
<tr>
<td colspan="2">
<tr-info-row class="tr-info-row">
<tr-info-row v-for="(link,index) in infoModal.links" class="tr-info-row">
<tr-info-title class="tr-info-title">
<a-tag color="blue">Config</a-tag>
<a-tooltip title='{{ i18n "copy" }}'>
@@ -444,18 +432,6 @@
</template>
</a-modal>
<script>
function refreshIPs(email) {
return HttpUtil.post(`/panel/inbound/clientIps/${email}`).then((msg) => {
if (msg.success) {
try {
return JSON.parse(msg.obj).join(', ');
} catch (e) {
return msg.obj;
}
}
});
}
const infoModal = {
visible: false,
inbound: new Inbound(),
@@ -470,7 +446,6 @@
isExpired: false,
subLink: '',
subJsonLink: '',
clientIps: '',
show(dbInbound, index) {
this.index = index;
this.inbound = dbInbound.toInbound();
@@ -478,12 +453,6 @@
this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index) : this.dbInbound.isExpiry;
this.clientStats = this.inbound.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
if (app.ipLimitEnable && this.clientSettings.limitIp) {
refreshIPs(this.clientStats.email).then((ips) => {
this.clientIps = ips;
})
}
if (this.inbound.protocol == Protocols.WIREGUARD) {
this.links = this.inbound.genInboundLinks(dbInbound.remark).split('\r\n')
} else {
@@ -512,7 +481,6 @@
el: '#inbound-info-modal',
data: {
infoModal,
refreshing: false,
get dbInbound() {
return this.infoModal.dbInbound;
},
@@ -549,26 +517,6 @@
remained = this.infoModal.clientStats.total - this.infoModal.clientStats.up - this.infoModal.clientStats.down;
return remained > 0 ? sizeFormat(remained) : '-';
},
refreshIPs() {
this.refreshing = true;
refreshIPs(this.infoModal.clientStats.email)
.then((ips) => {
this.infoModal.clientIps = ips;
})
.finally(() => {
this.refreshing = false;
});
},
clearClientIps() {
HttpUtil.post(`/panel/inbound/clearClientIps/${this.infoModal.clientStats.email}`)
.then((msg) => {
if (!msg.success) {
return;
}
this.infoModal.clientIps = 'No IP Record';
})
.catch(() => {});
},
},
});
</script>

View File

@@ -14,7 +14,6 @@
confirmLoading: false,
okText: '{{ i18n "sure" }}',
isEdit: false,
isGroup: false,
confirm: null,
inbound: new Inbound(),
dbInbound: new DBInbound(),
@@ -62,9 +61,6 @@
get isEdit() {
return inModal.isEdit;
},
get isGroup() {
return inModal.isGroup;
},
get client() {
return inModal.inbound.clients[0];
},
@@ -106,6 +102,11 @@
client.flow = "";
});
}
if ((this.inModal.inbound.protocol == Protocols.VLESS || this.inModal.inbound.protocol == Protocols.TROJAN) && !inModal.inbound.xtls) {
this.inModal.inbound.settings.vlesses.forEach(client => {
client.flow = "";
});
}
},
SSMethodChange() {
if (this.inModal.inbound.isSSMultiUser) {
@@ -131,6 +132,10 @@
inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert;
inModal.inbound.stream.tls.certs[index].keyFile = app.defaultKey;
},
setDefaultCertXtls(index) {
inModal.inbound.stream.xtls.certs[index].certFile = app.defaultCert;
inModal.inbound.stream.xtls.certs[index].keyFile = app.defaultKey;
},
async getNewX25519Cert() {
inModal.loading(true);
const msg = await HttpUtil.post('/server/getNewX25519Cert');

View File

@@ -15,7 +15,7 @@
overflow-y: hidden;
}
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper {
margin:-10px 22px !important;
margin:-10px 22px -10px !important;
}
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper .ant-table {
border-bottom-left-radius: 1rem;
@@ -40,7 +40,7 @@
padding: .5rem;
}
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper {
margin:-10px 2px !important;
margin:-10px 2px -10px !important;
}
}
.ant-col-sm-24 {
@@ -224,10 +224,6 @@
<a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item>
<a-menu-item v-if="subSettings.enable && dbInbounds.length > 0" key="addGroupClient">
<a-icon type="usergroup-add"></a-icon>
{{ i18n "pages.client.groupAdd"}}
</a-menu-item>
</a-menu>
</a-dropdown>
</a-col>
@@ -342,6 +338,7 @@
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="blue">TLS</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXtls" color="blue">XTLS</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="blue">Reality</a-tag>
</template>
</template>
@@ -552,7 +549,7 @@
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/clipboard/clipboard.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/uri/URI.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/inbound.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/xray.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/dbinbound.js?{{ .cur_ver }}"></script>
{{template "component/themeSwitcher" .}}
{{template "component/persianDatepicker" .}}
@@ -736,7 +733,7 @@
this.inbounds.push(to_inbound);
this.dbInbounds.push(dbInbound);
if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound.protocol)) {
if (dbInbound.isSS && (!to_inbound.isSSMultiUser)) {
if (inbound.protocol === Protocols.SHADOWSOCKS && (!to_inbound.isSSMultiUser)) {
continue;
}
this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);
@@ -863,9 +860,6 @@
case "delDepletedClients":
this.delDepletedClients(-1)
break;
case "addGroupClient":
this.openGroupAddClient()
break;
}
},
clickAction(action, dbInbound) {
@@ -941,7 +935,6 @@
settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
streamSettings: baseInbound.stream.toString(),
sniffing: baseInbound.sniffing.toString(),
allocate: baseInbound.allocate.toString(),
};
await this.submit('/panel/inbound/add', data, inModal);
},
@@ -987,7 +980,6 @@
};
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
data.sniffing = inbound.sniffing.toString();
data.allocate = inbound.allocate.toString();
await this.submit('/panel/inbound/add', data, inModal);
},
@@ -1007,25 +999,9 @@
};
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
data.sniffing = inbound.sniffing.toString();
data.allocate = inbound.allocate.toString();
await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal);
},
openGroupAddClient() {
clientModal.show({
title: '{{ i18n "pages.client.groupAdd"}}',
okText: '{{ i18n "pages.client.submitAdd"}}',
dbInbounds: this.dbInbounds,
confirm: async (clients, dbInboundIds) => {
await this.addGroupClient(clients, dbInboundIds, clientModal).then((res) => {
if(res){
this.showQrcode(dbInboundIds[0],clients[0], true)
}
});
},
isEdit: false
});
},
openAddClient(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clientModal.show({
@@ -1033,11 +1009,7 @@
okText: '{{ i18n "pages.client.submitAdd"}}',
dbInbound: dbInbound,
confirm: async (clients, dbInboundId) => {
await this.addClient(clients, dbInboundId, clientModal).then((res) => {
if(res){
this.showQrcode(dbInboundId,clients)
}
});
await this.addClient(clients, dbInboundId, clientModal);
},
isEdit: false
});
@@ -1060,7 +1032,6 @@
clientModal.show({
title: '{{ i18n "pages.client.edit"}}',
okText: '{{ i18n "pages.client.submitEdit"}}',
dbInbounds: this.dbInbounds,
dbInbound: dbInbound,
index: index,
confirm: async (client, dbInboundId, clientId) => {
@@ -1086,36 +1057,12 @@
};
await this.submit(`/panel/inbound/addClient`, data, modal);
},
async addGroupClient(clients, dbInboundIds, modal) {
const data = []
dbInboundIds.forEach((dbInboundId, index) => {
data.push({
id: dbInboundId,
settings: '{"clients": [' + clients[index].toString() + ']}',
})
})
return await this.submit(`/panel/inbound/addGroupClient`, data, modal, true)
},
async updateClient(client, dbInboundId, clientId) {
if (Array.isArray(client) && Array.isArray(dbInboundId) && Array.isArray(clientId)){
const data = []
client.forEach((client, index) => {
data.push({
clientId: clientId[index],
inbound: {
id: dbInboundId[index],
settings: '{"clients": [' + client.toString() + ']}',
}
})
})
await this.submit(`/panel/inbound/updateClients`, data, clientModal, true);
}else{
const data = {
id: dbInboundId,
settings: '{"clients": [' + client.toString() + ']}',
};
await this.submit(`/panel/inbound/updateClient/${clientId}`, data, clientModal);
}
const data = {
id: dbInboundId,
settings: '{"clients": [' + client.toString() + ']}',
};
await this.submit(`/panel/inbound/updateClient/${clientId}`, data, clientModal);
},
resetTraffic(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -1143,100 +1090,22 @@
onOk: () => this.submit('/panel/inbound/del/' + dbInboundId),
});
},
async delClientHandler(dbInboundId, currentClient, clients = []) {
if (clients.length > 0) {
const deleteRequestData = [];
for (const client of clients) {
const dbInbound = this.dbInbounds.find(inbound => inbound.id === client.inboundId);
if (dbInbound) {
const inbound = dbInbound.toInbound();
if (inbound && inbound.clients && inbound.clients.length === 1) {
let newClient = Inbound.Settings.getSettings(inbound.protocol).toString();
newClient = JSON.parse(newClient);
newClient = newClient && newClient.clients && newClient.clients.length > 0 ? JSON.stringify(newClient.clients[0], null, 2) : null;
if (newClient) {
const data = {
id: client.inboundId,
settings: '{"clients": [' + newClient + ']}',
};
await this.submit(`/panel/inbound/addClient`, data, null);
}
}
deleteRequestData.push({
inboundId: client.inboundId,
clientId: client.clientId,
});
}
}
await this.submit('/panel/inbound/delGroupClients', deleteRequestData, null, true);
} else {
const dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
const clientId = this.getClientId(dbInbound.protocol, currentClient);
await this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`);
}
},
delClient(dbInboundId, currentClient, confirmation = true) {
const subGroup = this.subSettings.enable ? this.getSubGroupClients(this.dbInbounds, currentClient) : [];
const clients = subGroup && subGroup.clients && subGroup.clients.length > 1 ? subGroup.clients : [];
delClient(dbInboundId, client,confirmation = true) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clientId = this.getClientId(dbInbound.protocol, client);
if (confirmation){
const clientEmails = clients.length > 0 ? clients.map(item => item.email) : currentClient.email
this.$confirm({
title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + clientEmails,
title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + client.email,
content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
class: themeSwitcher.currentTheme,
okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.delClientHandler(dbInboundId, currentClient, clients),
onOk: () => this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`),
});
} else {
this.delClientHandler(dbInboundId, currentClient, clients)
this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`);
}
},
getSubGroupClients(dbInbounds, currentClient) {
const response = {
inbounds: [],
clients: [],
editIds: [],
};
if (!Array.isArray(dbInbounds) || dbInbounds.length === 0) {
return response;
}
if (!currentClient || !currentClient.subId) {
return response;
}
dbInbounds.forEach((dbInboundItem) => {
try {
const dbInbound = new DBInbound(dbInboundItem);
if (!dbInbound) {
return;
}
const inbound = dbInbound.toInbound();
if (!inbound || !Array.isArray(inbound.clients)) {
return;
}
inbound.clients.forEach((client) => {
if (client.subId === currentClient.subId) {
client.inboundId = dbInboundItem.id;
client.clientId = this.getClientId(dbInbound.protocol, client);
response.inbounds.push(dbInboundItem.id);
response.clients.push(client);
response.editIds.push(client.clientId);
}
});
} catch (error) {
console.error("Error processing dbInboundItem:", dbInboundItem, error);
}
});
return response;
},
getClientId(protocol, client) {
switch (protocol) {
case Protocols.TROJAN: return client.password;
@@ -1264,10 +1133,10 @@
}
return newDbInbound;
},
showQrcode(dbInboundId, client, isJustSub = false) {
showQrcode(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
newDbInbound = this.checkFallback(dbInbound);
qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client, isJustSub);
qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client);
},
showInfo(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -1287,61 +1156,36 @@
},
async switchEnableClient(dbInboundId, client) {
this.loading()
const subGroup = this.subSettings.enable ? this.getSubGroupClients(this.dbInbounds, client) : [];
if (subGroup && subGroup.clients && subGroup.clients.length > 0){
await this.updateClient(subGroup.clients.map(item => {
item.enable = !item.enable
return item
}), subGroup.inbounds, subGroup.editIds);
}else{
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
inbound = dbInbound.toInbound();
clients = inbound.clients;
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
clients[index].enable = !clients[index].enable;
clientId = this.getClientId(dbInbound.protocol, clients[index]);
await this.updateClient(clients[index], dbInboundId, clientId);
}
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
inbound = dbInbound.toInbound();
clients = inbound.clients;
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
clients[index].enable = !clients[index].enable;
clientId = this.getClientId(dbInbound.protocol, clients[index]);
await this.updateClient(clients[index], dbInboundId, clientId);
this.loading(false);
},
async submit(url, data, model, isJson = false) {
const msg = isJson ? await HttpUtil.postWithModalJson(url, data, model) : await HttpUtil.postWithModal(url, data, model);
async submit(url, data, modal) {
const msg = await HttpUtil.postWithModal(url, data, modal);
if (msg.success) {
await this.getDBInbounds();
}
return msg
},
getInboundClients(dbInbound) {
return dbInbound.toInbound().clients;
},
resetClientTrafficHandler(client, dbInboundId, clients = []) {
if (clients.length > 0){
const resetRequests = clients
.filter(client => {
const inbound = this.dbInbounds.find(inbound => inbound.id === client.inboundId);
return inbound && this.hasClientStats(inbound, client.email);
}).map(client => ({ inboundId: client.inboundId, email: client.email}));
this.submit('/panel/inbound/resetGroupClientTraffic', resetRequests, null, true)
}else {
this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email);
}
},
resetClientTraffic(client, dbInboundId, confirmation = true) {
const subGroup = this.subSettings.enable ? this.getSubGroupClients(this.dbInbounds, client) : [];
const clients = subGroup && subGroup.clients && subGroup.clients.length > 1 ? subGroup.clients : [];
if (confirmation){
const clientEmails = clients.length > 0 ? clients.map(item => item.email) : client.email
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' ' + clientEmails,
title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' ' + client.email,
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.resetClientTrafficHandler(client, dbInboundId, clients),
onOk: () => this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email),
})
} else {
this.resetClientTrafficHandler(client, dbInboundId, clients);
this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email);
}
},
resetAllTraffic() {
@@ -1377,10 +1221,6 @@
isExpiry(dbInbound, index) {
return dbInbound.toInbound().isExpiry(index);
},
hasClientStats(dbInbound, email) {
if (email.length == 0) return 0;
return !!dbInbound.clientStats.find(stats => stats.email === email);
},
getUpStats(dbInbound, email) {
if (email.length == 0) return 0;
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
@@ -1468,7 +1308,7 @@
if (clients != null){
clients.forEach(c => {
if (c.subId && c.subId.length>0){
subLinks.push(this.subSettings.subURI + c.subId)
subLinks.push(this.subSettings.subURI + c.subId + "?name=" + c.subId)
}
})
}
@@ -1495,7 +1335,7 @@
if (clients != null){
clients.forEach(c => {
if (c.subId && c.subId.length>0){
subLinks.push(this.subSettings.subURI + c.subId)
subLinks.push(this.subSettings.subURI + c.subId + "?name=" + c.subId)
}
})
}

View File

@@ -291,7 +291,6 @@
<a-select-option value="20">20</a-select-option>
<a-select-option value="50">50</a-select-option>
<a-select-option value="100">100</a-select-option>
<a-select-option value="500">500</a-select-option>
</a-select>
<a-select size="small" v-model="logModal.level" style="width:95px;"
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">

View File

@@ -26,7 +26,7 @@
padding: .5rem 1rem;
text-align: center;
background: rgb(255 145 0 / 15%);
margin: 1.5rem 2.5rem 0rem;
margin: 1.5rem 2.5rem 0rem 2.5rem;
border-radius: .5rem;
transition: all 0.5s;
animation: signal 3s cubic-bezier(0.18, 0.89, 0.32, 1.28) infinite;
@@ -138,7 +138,7 @@
<setting-list-item type="text" title='{{ i18n "pages.settings.publicKeyPath"}}' desc='{{ i18n "pages.settings.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.privateKeyPath"}}' desc='{{ i18n "pages.settings.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.panelUrlPath"}}' desc='{{ i18n "pages.settings.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.sessionMaxAge" }}' desc='{{ i18n "pages.settings.sessionMaxAgeDesc" }}' v-model="allSetting.sessionMaxAge" :min="60"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.sessionMaxAge" }}' desc='{{ i18n "pages.settings.sessionMaxAgeDesc" }}' v-model="allSetting.sessionMaxAge" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.pageSize" }}' desc='{{ i18n "pages.settings.pageSizeDesc" }}' v-model="allSetting.pageSize" :min="0" :step="5"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.expireTimeDiff" }}' desc='{{ i18n "pages.settings.expireTimeDiffDesc" }}' v-model="allSetting.expireDiff" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.trafficDiff" }}' desc='{{ i18n "pages.settings.trafficDiffDesc" }}' v-model="allSetting.trafficDiff" :min="0"></setting-list-item>
@@ -246,7 +246,6 @@
<setting-list-item type="switch" title='{{ i18n "pages.settings.tgNotifyLogin" }}' desc='{{ i18n "pages.settings.tgNotifyLoginDesc" }}' v-model="allSetting.tgBotLoginNotify"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.tgNotifyCpu" }}' desc='{{ i18n "pages.settings.tgNotifyCpuDesc" }}' v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramProxy"}}' desc='{{ i18n "pages.settings.telegramProxyDesc"}}' v-model="allSetting.tgBotProxy" placeholder="socks5://user:pass@host:port"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramAPIServer"}}' desc='{{ i18n "pages.settings.telegramAPIServerDesc"}}' v-model="allSetting.tgBotAPIServer" placeholder="https://api.example.com"></setting-list-item>
<a-list-item>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
@@ -268,7 +267,6 @@
<a-tab-pane key="4" tab='{{ i18n "pages.settings.subSettings" }}'>
<a-list item-layout="horizontal">
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.subSyncEnable"}}' desc='{{ i18n "pages.settings.subSyncEnableDesc"}}' v-model="allSetting.subSyncEnable"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEncrypt"}}' desc='{{ i18n "pages.settings.subEncryptDesc"}}' v-model="allSetting.subEncrypt"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.subShowInfo"}}' desc='{{ i18n "pages.settings.subShowInfoDesc"}}' v-model="allSetting.subShowInfo"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item>
@@ -307,38 +305,20 @@
<a-list-item style="padding: 20px">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='Noises'>
<template slot="description">{{ i18n "pages.settings.noisesDesc"}}</template>
<a-list-item-meta title='Noise'>
<template slot="description">{{ i18n "pages.settings.noiseDesc"}}</template>
</a-list-item-meta>
</a-col>
<a-col :lg="24" :xl="12">
<a-switch v-model="noises"></a-switch>
<a-switch v-model="noise"></a-switch>
</a-col>
</a-row>
<a-collapse v-if="noises" style="margin-top: 14px;">
<a-collapse-panel v-for="(noise, index) in noisesArray" :key="index" :header="`Noise ${index + 1}`">
<a-list-item style="padding: 10px 20px">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='Type'></a-list-item-meta>
</a-col>
<a-col :lg="24" :xl="12">
<a-select :value="noise.type" style="width: 100%" :dropdown-class-name="themeSwitcher.currentTheme"
@change="(value) => updateNoiseType(index, value)">
<a-select-option :value="p" :label="p" v-for="p in ['rand', 'base64', 'str']" :key="p">
[[ p ]] </a-select-option>
</a-select>
</a-col>
</a-row>
</a-list-item>
<setting-list-item style="padding: 10px 20px" type="text" title='Packet' :value="noise.packet"
@input="(value) => updateNoisePacket(index, value)" placeholder="5-10"></setting-list-item>
<setting-list-item style="padding: 10px 20px" type="text" title='Delay (ms)' :value="noise.delay"
@input="(value) => updateNoiseDelay(index, value)" placeholder="10-20"></setting-list-item>
<a-button v-if="noisesArray.length > 1" type="danger" @click="removeNoise(index)">Remove</a-button>
<a-collapse v-if="noise" style="margin-top: 14px;">
<a-collapse-panel header='{{ i18n "pages.settings.noiseSett"}}' v-if="noise">
<setting-list-item style="padding: 10px 20px" type="text" title='Packet (ms)' v-model="noisePacket" placeholder="rand:5-10"></setting-list-item>
<setting-list-item style="padding: 10px 20px" type="text" title='Delay (ms)' v-model="noiseDelay" placeholder="10-20"></setting-list-item>
</a-collapse-panel>
</a-collapse>
<a-button v-if="noises" type="primary" @click="addNoise" style="margin-top: 10px">Add Noise</a-button>
</a-list-item>
<a-list-item style="padding: 20px">
<a-row>
@@ -382,40 +362,9 @@
</a-col>
</a-row>
<a-collapse v-if="enableDirect" style="margin-top: 14px;">
<a-collapse-panel header='{{ i18n "pages.settings.direct"}}'>
<a-list-item>
<a-row style="padding: 0 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta
title='{{ i18n "pages.xray.directips" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<a-select mode="tags" style="width: 100%"
v-model="directIPs"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label"
v-for="p in directIPsOptions"> [[ p.label ]]
</a-select-option>
</a-select>
</a-col>
</a-row>
</a-list-item>
<a-list-item>
<a-row style="padding: 0 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta
title='{{ i18n "pages.xray.directdomains" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<a-select mode="tags" style="width: 100%"
v-model="directDomains"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label"
v-for="p in diretDomainsOptions"> [[ p.label ]]
</a-select-option>
</a-select>
</a-col>
</a-row>
<a-collapse-panel header='{{ i18n "pages.settings.directSett"}}'>
<a-list-item style="padding: 10px 20px">
<a-checkbox-group v-model="directCountries" name="Countries" :options="countryOptions"></a-checkbox-group>
</a-list-item>
</a-collapse-panel>
</a-collapse>
@@ -466,18 +415,19 @@
sockopt: {
tcpKeepAliveIdle: 100,
tcpMptcp: true,
penetrate: true
tcpNoDelay: true
}
}
},
defaultNoises: {
tag: "noises",
defaultNoise: {
tag: "noise",
protocol: "freedom",
settings: {
domainStrategy: "AsIs",
noises: [
{ type: "rand", packet: "10-20", delay: "10-16" },
],
noise: {
packet: "rand:5-10",
delay: "10-20",
}
},
},
defaultMux: {
@@ -491,38 +441,27 @@
type: "field",
outboundTag: "direct",
domain: [
"geosite:category-ir"
]
"geosite:category-ir",
"geosite:cn"
],
"enabled": true
},
{
type: "field",
outboundTag: "direct",
ip: [
"geoip:private",
"geoip:ir"
]
"geoip:ir",
"geoip:cn"
],
enabled: true
},
],
directIPsOptions: [
{ label: 'Private IP', value: 'geoip:private' },
{ label: '🇮🇷 Iran', value: 'geoip:ir' },
{ label: '🇨🇳 China', value: 'geoip:cn' },
{ label: '🇷🇺 Russia', value: 'geoip:ru' },
{ label: '🇻🇳 Vietnam', value: 'geoip:vn' },
{ label: '🇪🇸 Spain', value: 'geoip:es' },
{ label: '🇮🇩 Indonesia', value: 'geoip:id' },
{ label: '🇺🇦 Ukraine', value: 'geoip:ua' },
{ label: '🇹🇷 Türkiye', value: 'geoip:tr' },
{ label: '🇧🇷 Brazil', value: 'geoip:br' },
],
diretDomainsOptions: [
{ label: 'Private DNS', value: 'geosite:private' },
{ label: '🇮🇷 Iran', value: 'geosite:category-ir' },
{ label: '🇨🇳 China', value: 'geosite:cn' },
{ label: '🇷🇺 Russia', value: 'geosite:category-ru' },
{ label: 'Apple', value: 'geosite:apple' },
{ label: 'Meta', value: 'geosite:meta' },
{ label: 'Google', value: 'geosite:google' },
countryOptions: [
{ label: 'Private IP/Domain', value: 'private' },
{ label: '🇮🇷 Iran', value: 'ir' },
{ label: '🇨🇳 China', value: 'cn' },
{ label: '🇷🇺 Russia', value: 'ru' },
],
get remarkModel() {
rm = this.allSetting.remarkModel;
@@ -652,30 +591,6 @@
this.user.loginSecret = "";
}
},
addNoise() {
const newNoise = { type: "rand", packet: "10-20", delay: "10-16" };
this.noisesArray = [...this.noisesArray, newNoise];
},
removeNoise(index) {
const newNoises = [...this.noisesArray];
newNoises.splice(index, 1);
this.noisesArray = newNoises;
},
updateNoiseType(index, value) {
const updatedNoises = [...this.noisesArray];
updatedNoises[index] = { ...updatedNoises[index], type: value };
this.noisesArray = updatedNoises;
},
updateNoisePacket(index, value) {
const updatedNoises = [...this.noisesArray];
updatedNoises[index] = { ...updatedNoises[index], packet: value };
this.noisesArray = updatedNoises;
},
updateNoiseDelay(index, value) {
const updatedNoises = [...this.noisesArray];
updatedNoises[index] = { ...updatedNoises[index], delay: value };
this.noisesArray = updatedNoises;
},
},
computed: {
fragment: {
@@ -714,27 +629,29 @@
}
}
},
noises: {
get() {
return this.allSetting?.subJsonNoises != "";
},
set(v) {
if (v) {
this.allSetting.subJsonNoises = JSON.stringify(this.defaultNoises);
} else {
this.allSetting.subJsonNoises = "";
noise: {
get: function () { return this.allSetting?.subJsonNoise != ""; },
set: function (v) {
this.allSetting.subJsonNoise = v ? JSON.stringify(this.defaultNoise) : "";
}
},
noisePacket: {
get: function () { return this.noise ? JSON.parse(this.allSetting.subJsonNoise).settings.noise.packet : ""; },
set: function (v) {
if (v != "") {
newNoise = JSON.parse(this.allSetting.subJsonNoise);
newNoise.settings.noise.packet = v;
this.allSetting.subJsonNoise = JSON.stringify(newNoise);
}
}
},
noisesArray: {
get() {
return this.noises ? JSON.parse(this.allSetting.subJsonNoises).settings.noises : [];
},
set(value) {
if (this.noises) {
const newNoises = JSON.parse(this.allSetting.subJsonNoises);
newNoises.settings.noises = value;
this.allSetting.subJsonNoises = JSON.stringify(newNoises);
noiseDelay: {
get: function () { return this.noise ? JSON.parse(this.allSetting.subJsonNoise).settings.noise.delay : ""; },
set: function (v) {
if (v != "") {
newNoise = JSON.parse(this.allSetting.subJsonNoise);
newNoise.settings.noise.delay = v;
this.allSetting.subJsonNoise = JSON.stringify(newNoise);
}
}
},
@@ -774,51 +691,29 @@
this.allSetting.subJsonRules = v ? JSON.stringify(this.defaultRules) : "";
}
},
directIPs: {
directCountries: {
get: function () {
if (!this.enableDirect) return [];
const rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return [];
const ipRule = rules.find(r => r.ip);
return ipRule?.ip ?? [];
rules = JSON.parse(this.allSetting.subJsonRules);
return Array.isArray(rules) ? rules[1].ip.map(d => d.replace("geoip:", "")) : [];
},
set: function (v) {
let rules = JSON.parse(this.allSetting.subJsonRules);
rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return;
if (v.length == 0) {
rules = rules.filter(r => !r.ip);
} else {
let ruleIndex = rules.findIndex(r => r.ip);
if (ruleIndex == -1) ruleIndex = rules.push(this.defaultRules[1]) - 1;
rules[ruleIndex].ip = [];
v.forEach(d => {
rules[ruleIndex].ip.push(d);
});
}
this.allSetting.subJsonRules = JSON.stringify(rules);
}
},
directDomains: {
get: function () {
if (!this.enableDirect) return [];
const rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return [];
const domainRule = rules.find(r => r.domain);
return domainRule?.domain ?? [];
},
set: function (v) {
let rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return;
if (v.length == 0) {
rules = rules.filter(r => !r.domain);
} else {
let ruleIndex = rules.findIndex(r => r.domain);
if (ruleIndex == -1) ruleIndex = rules.push(this.defaultRules[0]) - 1;
rules[ruleIndex].domain = v;
}
rules[0].domain = [];
rules[1].ip = [];
v.forEach(d => {
let category = '';
if (["cn", "private"].includes(d)) {
category = "";
} else if (d === 'ru') {
category = "category-gov-";
} else {
category = "category-";
}
rules[0].domain.push("geosite:" + category + d);
rules[1].ip.push("geoip:" + d);
});
this.allSetting.subJsonRules = JSON.stringify(rules);
}
},
@@ -850,4 +745,4 @@
});
</script>
</body>
</html>
</html>

View File

@@ -147,7 +147,7 @@
publicKey: peer.public_key,
endpoint: peer.endpoint.host,
}],
noKernelTun: false,
kernelMode: false
}
});
}

View File

@@ -52,7 +52,7 @@
font-size: 24px;
}
.ant-collapse-content-box>li {
padding: 12px 0 0 !important;
padding: 12px 0 0 0 !important;
}
.ant-list-item>li {
padding: 10px 20px !important;
@@ -143,7 +143,6 @@
</a-select>
</a-col>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.xray.outboundTraffic"}}' desc='{{ i18n "pages.xray.outboundTrafficDesc"}}' v-model="outboundTraffic"></setting-list-item>
</a-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.logConfigs" }}'>
@@ -164,8 +163,8 @@
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select v-model="logLevel" :dropdown-class-name="themeSwitcher.currentTheme" style="width: 100%">
<a-select-option v-for="s in log.loglevel" :value="s">[[ s ]]</a-select-option>
<a-select v-model="setLogLevel" :dropdown-class-name="themeSwitcher.currentTheme" style="width: 100%">
<a-select-option v-for="s in logLevel" :value="s">[[ s ]]</a-select-option>
</a-select>
</template>
</a-col>
@@ -179,8 +178,7 @@
<a-col :lg="24" :xl="12">
<template>
<a-select v-model="accessLog" :dropdown-class-name="themeSwitcher.currentTheme" style="width: 100%">
<a-select-option value=''>Empty</a-select-option>
<a-select-option v-for="s in log.access" :value="s">[[ s ]]</a-select-option>
<a-select-option v-for="s in access" :key="s" :value="s">[[ s ]]</a-select-option>
</a-select>
</template>
</a-col>
@@ -194,28 +192,11 @@
<a-col :lg="24" :xl="12">
<template>
<a-select v-model="errorLog" :dropdown-class-name="themeSwitcher.currentTheme" style="width: 100%">
<a-select-option value=''>Empty</a-select-option>
<a-select-option v-for="s in log.error" :value="s">[[ s ]]</a-select-option>
<a-select-option v-for="s in error" :key="s" :value="s">[[ s ]]</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
<a-row style="padding: 10px 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.xray.maskAddress" }}'
description='{{ i18n "pages.xray.maskAddressDesc" }}'>
</a-list-item-meta>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select v-model="maskAddressLog" :dropdown-class-name="themeSwitcher.currentTheme" style="width: 100%">
<a-select-option value=''>Empty</a-select-option>
<a-select-option v-for="s in log.maskAddress" :value="s">[[ s ]]</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.xray.dnsLog"}}' desc='{{ i18n "pages.xray.dnsLogDesc"}}' v-model="dnslog"></setting-list-item>
</a-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.blockConfigs"}}'>
@@ -229,157 +210,85 @@
</a-row>
<a-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.Torrent"}}' desc='{{ i18n "pages.xray.TorrentDesc"}}' v-model="torrentSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.PrivateIp"}}' desc='{{ i18n "pages.xray.PrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.Ads"}}' desc='{{ i18n "pages.xray.AdsDesc"}}' v-model="AdsSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.Family"}}' desc='{{ i18n "pages.xray.FamilyDesc"}}' v-model="familyProtectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.Security"}}' desc='{{ i18n "pages.xray.SecurityDesc"}}' v-model="SecuritySettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.Speedtest"}}' desc='{{ i18n "pages.xray.SpeedtestDesc"}}' v-model="SpeedTestSettings"></setting-list-item>
</a-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.basicRouting"}}'>
<a-collapse-panel header='{{ i18n "pages.xray.blockCountryConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" style="text-align: center;">
<template slot="message">
<a-icon type="exclamation-circle" theme="filled" style="color: #FFA031"></a-icon>
{{ i18n "pages.xray.blockConnectionsConfigsDesc" }}
{{ i18n "pages.xray.blockCountryConfigsDesc" }}
</template>
</a-alert>
</a-row>
<a-list-item>
<a-row style="padding: 0 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta
title='{{ i18n "pages.xray.blockips" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select mode="tags" v-model="blockedIPs" style="width: 100%"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label"
v-for="p in settingsData.IPsOptions"> [[ p.label ]]
</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
</a-list-item>
<a-list-item>
<a-row style="padding: 0 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta
title='{{ i18n "pages.xray.blockdomains" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select mode="tags" style="width: 100%"
v-model="blockedDomains"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label"
v-for="p in settingsData.BlockDomainsOptions"> [[ p.label ]]
</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.xray.IRIp"}}' desc='{{ i18n "pages.xray.IRIpDesc"}}' v-model="IRIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.IRDomain"}}' desc='{{ i18n "pages.xray.IRDomainDesc"}}' v-model="IRDomainSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.ChinaIp"}}' desc='{{ i18n "pages.xray.ChinaIpDesc"}}' v-model="ChinaIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.ChinaDomain"}}' desc='{{ i18n "pages.xray.ChinaDomainDesc"}}' v-model="ChinaDomainSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.RussiaIp"}}' desc='{{ i18n "pages.xray.RussiaIpDesc"}}' v-model="RussiaIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.RussiaDomain"}}' desc='{{ i18n "pages.xray.RussiaDomainDesc"}}' v-model="RussiaDomainSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.VNIp"}}' desc='{{ i18n "pages.xray.VNIpDesc"}}' v-model="VNIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.VNDomain"}}' desc='{{ i18n "pages.xray.VNDomainDesc"}}' v-model="VNDomainSettings"></setting-list-item>
</a-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.directCountryConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" style="text-align: center; margin-top: 20px;">
<a-alert type="warning" style="text-align: center;">
<template slot="message">
<a-icon type="exclamation-circle" theme="filled" style="color: #FFA031"></a-icon>
{{ i18n "pages.xray.directConnectionsConfigsDesc" }}
{{ i18n "pages.xray.directCountryConfigsDesc" }}
</template>
</a-alert>
</a-row>
<a-list-item>
<a-row style="padding: 0 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta
title='{{ i18n "pages.xray.directips" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select mode="tags" style="width: 100%"
v-model="directIPs"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label"
v-for="p in settingsData.IPsOptions"> [[ p.label ]]
</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
</a-list-item>
<a-list-item>
<a-row style="padding: 0 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta
title='{{ i18n "pages.xray.directdomains" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select mode="tags" style="width: 100%"
v-model="directDomains"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label"
v-for="p in settingsData.DomainsOptions"> [[ p.label ]]
</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.xray.DirectIRIp"}}' desc='{{ i18n "pages.xray.DirectIRIpDesc"}}' v-model="IRIpDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.DirectIRDomain"}}' desc='{{ i18n "pages.xray.DirectIRDomainDesc"}}' v-model="IRDomainDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.DirectChinaIp"}}' desc='{{ i18n "pages.xray.DirectChinaIpDesc"}}' v-model="ChinaIpDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.DirectChinaDomain"}}' desc='{{ i18n "pages.xray.DirectChinaDomainDesc"}}' v-model="ChinaDomainDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.DirectRussiaIp"}}' desc='{{ i18n "pages.xray.DirectRussiaIpDesc"}}' v-model="RussiaIpDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.DirectRussiaDomain"}}' desc='{{ i18n "pages.xray.DirectRussiaDomainDesc"}}' v-model="RussiaDomainDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.DirectVNIp"}}' desc='{{ i18n "pages.xray.DirectVNIpDesc"}}' v-model="VNIpDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.DirectVNDomain"}}' desc='{{ i18n "pages.xray.DirectVNDomainDesc"}}' v-model="VNDomainDirectSettings"></setting-list-item>
</a-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.ipv4Configs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" style="text-align: center; margin-top: 20px;">
<a-alert type="warning" style="text-align: center;">
<template slot="message">
<a-icon type="exclamation-circle" theme="filled" style="color: #FFA031"></a-icon>
{{ i18n "pages.xray.ipv4RoutingDesc" }}
{{ i18n "pages.xray.ipv4ConfigsDesc" }}
</template>
</a-alert>
</a-row>
<a-list-item>
<a-row style="padding: 0 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta
title='{{ i18n "pages.xray.ipv4Routing" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select mode="tags" style="width: 100%"
v-model="ipv4Domains"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label"
v-for="p in settingsData.ServicesOptions"> [[ p.label ]]
</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.xray.GoogleIPv4"}}' desc='{{ i18n "pages.xray.GoogleIPv4Desc"}}' v-model="GoogleIPv4Settings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.NetflixIPv4"}}' desc='{{ i18n "pages.xray.NetflixIPv4Desc"}}' v-model="NetflixIPv4Settings"></setting-list-item>
</a-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.warpConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" style="text-align: center; margin-top: 20px;">
<a-alert type="warning" style="text-align: center;">
<template slot="message">
<a-icon type="exclamation-circle" theme="filled" style="color: #FFA031"></a-icon>
{{ i18n "pages.xray.warpRoutingDesc" }}
{{ i18n "pages.xray.warpConfigsDesc" }}
</template>
</a-alert>
</a-row>
<a-list-item>
<template v-if="WarpExist">
<a-list-item>
<a-row style="padding: 0 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta
title='{{ i18n "pages.xray.warpRouting" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select mode="tags" style="width: 100%"
v-model="warpDomains"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label"
v-for="p in settingsData.ServicesOptions"> [[ p.label ]]
</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
</a-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.GoogleWARP"}}' desc='{{ i18n "pages.xray.GoogleWARPDesc"}}' v-model="GoogleWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.OpenAIWARP"}}' desc='{{ i18n "pages.xray.OpenAIWARPDesc"}}' v-model="OpenAIWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.NetflixWARP"}}' desc='{{ i18n "pages.xray.NetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.SpotifyWARP"}}' desc='{{ i18n "pages.xray.SpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.MetaWARP"}}' desc='{{ i18n "pages.xray.MetaWARPDesc"}}' v-model="MetaWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.AppleWARP"}}' desc='{{ i18n "pages.xray.AppleWARPDesc"}}' v-model="AppleWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.RedditWARP"}}' desc='{{ i18n "pages.xray.RedditWARPDesc"}}' v-model="RedditWARPSettings"></setting-list-item>
</template>
<a-button style="margin-left: 20px;" v-else type="primary" icon="cloud" @click="showWarp()">WARP</a-button>
</a-list-item>
@@ -660,90 +569,86 @@
</a-radio-group>
<textarea style="position:absolute; left: -800px;" id="obsSetting"></textarea>
</a-tab-pane>
<a-tab-pane key="tpl-dns" tab='DNS' style="padding-top: 20px;" force-render="true">
<setting-list-item type="switch" title='{{ i18n "pages.xray.dns.enable" }}'
desc='{{ i18n "pages.xray.dns.enableDesc" }}' v-model="enableDNS"></setting-list-item>
<a-tab-pane key="tpl-dns" tab='DNS' style="padding-top: 20px;" force-render="true">
<setting-list-item type="switch" title='{{ i18n "pages.xray.dns.enable" }}' desc='{{ i18n "pages.xray.dns.enableDesc" }}' v-model="enableDNS"></setting-list-item>
<template v-if="enableDNS">
<setting-list-item style="padding: 10px 20px" type="text" title='{{ i18n "pages.xray.dns.tag" }}'
desc='{{ i18n "pages.xray.dns.tagDesc" }}' v-model="dnsTag"></setting-list-item>
<a-list-item style="padding: 10px 20px">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.xray.dns.strategy" }}'
description='{{ i18n "pages.xray.dns.strategyDesc" }}' />
</a-col>
<a-col :lg="24" :xl="12">
<a-select v-model="dnsStrategy" style="width: 100%"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l" :label="l" v-for="l in ['UseIP', 'UseIPv4', 'UseIPv6']">
[[ l ]]
</a-select-option>
</a-select>
</a-col>
</a-row>
</a-list-item>
<a-divider>DNS</a-divider>
<a-button type="primary" icon="plus" @click="addDNSServer()" style="margin-bottom: 10px;">{{ i18n
"pages.xray.dns.add" }}</a-button>
<a-table :columns="dnsColumns" bordered v-if="dnsServers.length>0" :row-key="r => r.key"
:data-source="dnsServers" :scroll="isMobile ? {} : { x: 200 }" :pagination="false" :indent-size="0"
:style="isMobile ? 'padding: 5px 0' : 'margin-left: 1px;'">
<template slot="action" slot-scope="text,dns,index">
[[ index+1 ]]
<a-dropdown :trigger="['click']">
<a-icon @click="e => e.preventDefault()" type="more"
style="font-size: 16px; text-decoration: bold;"></a-icon>
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
<a-menu-item @click="editDNSServer(index)">
<a-icon type="edit"></a-icon>
{{ i18n "edit" }}
</a-menu-item>
<a-menu-item @click="deleteDNSServer(index)">
<span style="color: #FF4D4F">
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
</span>
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
<template slot="address" slot-scope="dns,index">
<span v-if="typeof dns == 'object'">[[ dns.address ]]</span>
<span v-else>[[ dns ]]</span>
</template>
<template slot="domain" slot-scope="dns,index">
<span v-if="typeof dns == 'object'">[[ dns.domains.join(",") ]]</span>
</template>
<template slot="expectIPs" slot-scope="dns,index">
<span v-if="typeof dns == 'object'">[[ dns.expectIPs.join(",") ]]</span>
</template>
</a-table>
<a-divider>Fake DNS</a-divider>
<a-button type="primary" icon="plus" @click="addFakedns()" style="margin-bottom: 10px;">{{ i18n
"pages.xray.fakedns.add" }}</a-button>
<a-table :columns="fakednsColumns" bordered v-if="fakeDns && fakeDns.length>0" :row-key="r => r.key"
:data-source="fakeDns" :scroll="isMobile ? {} : { x: 200 }" :pagination="false" :indent-size="0"
:style="isMobile ? 'padding: 5px 0' : 'margin-left: 1px;'">
<template slot="action" slot-scope="text,fakedns,index">
[[ index+1 ]]
<a-dropdown :trigger="['click']">
<a-icon @click="e => e.preventDefault()" type="more"
style="font-size: 16px; text-decoration: bold;"></a-icon>
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
<a-menu-item @click="editFakedns(index)">
<a-icon type="edit"></a-icon>
{{ i18n "edit" }}
</a-menu-item>
<a-menu-item @click="deleteFakedns(index)">
<span style="color: #FF4D4F">
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
</span>
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
</a-table>
<setting-list-item style="padding: 10px 20px" type="text" title='{{ i18n "pages.xray.dns.tag" }}' desc='{{ i18n "pages.xray.dns.tagDesc" }}' v-model="dnsTag"></setting-list-item>
<a-list-item style="padding: 10px 20px">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.xray.dns.strategy" }}' description='{{ i18n "pages.xray.dns.strategyDesc" }}' />
</a-col>
<a-col :lg="24" :xl="12">
<a-select
v-model="dnsStrategy"
style="width: 100%"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l" :label="l" v-for="l in ['UseIP', 'UseIPv4', 'UseIPv6']">
[[ l ]]
</a-select-option>
</a-select>
</a-col>
</a-row>
</a-list-item>
<a-divider>DNS</a-divider>
<a-button type="primary" icon="plus" @click="addDNSServer()" style="margin-bottom: 10px;">{{ i18n "pages.xray.dns.add" }}</a-button>
<a-table :columns="dnsColumns" bordered v-if="dnsServers.length>0"
:row-key="r => r.key"
:data-source="dnsServers"
:scroll="isMobile ? {} : { x: 200 }"
:pagination="false"
:indent-size="0"
:style="isMobile ? 'padding: 5px 0' : 'margin-left: 1px;'">
<template slot="action" slot-scope="text,dns,index">
[[ index+1 ]]
<a-dropdown :trigger="['click']">
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;"></a-icon>
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
<a-menu-item @click="editDNSServer(index)">
<a-icon type="edit"></a-icon>
{{ i18n "edit" }}
</a-menu-item>
<a-menu-item @click="deleteDNSServer(index)">
<span style="color: #FF4D4F">
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
</span>
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
<template slot="address" slot-scope="dns,index">
<span v-if="typeof dns == 'object'">[[ dns.address ]]</span>
<span v-else>[[ dns ]]</span>
</template>
<template slot="domain" slot-scope="dns,index">
<span v-if="typeof dns == 'object'">[[ dns.domains.join(",") ]]</span>
</template>
</a-table>
<a-divider>Fake DNS</a-divider>
<a-button type="primary" icon="plus" @click="addFakedns()" style="margin-bottom: 10px;">{{ i18n "pages.xray.fakedns.add" }}</a-button>
<a-table :columns="fakednsColumns" bordered v-if="fakeDns && fakeDns.length>0" :row-key="r => r.key"
:data-source="fakeDns" :scroll="isMobile ? {} : { x: 200 }" :pagination="false" :indent-size="0"
:style="isMobile ? 'padding: 5px 0' : 'margin-left: 1px;'">
<template slot="action" slot-scope="text,fakedns,index">
[[ index+1 ]]
<a-dropdown :trigger="['click']">
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;"></a-icon>
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
<a-menu-item @click="editFakedns(index)">
<a-icon type="edit"></a-icon>
{{ i18n "edit" }}
</a-menu-item>
<a-menu-item @click="deleteFakedns(index)">
<span style="color: #FF4D4F">
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
</span>
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
</a-table>
</template>
</a-tab-pane>
</a-tab-pane>
<a-tab-pane key="tpl-advanced" tab='{{ i18n "pages.xray.advancedTemplate"}}' style="padding-top: 20px;" force-render="true">
<a-list-item-meta title='{{ i18n "pages.xray.Template"}}' description='{{ i18n "pages.xray.TemplateDesc"}}'></a-list-item-meta>
<a-radio-group v-model="advSettings" @change="changeCode" button-style="solid" style="margin: 10px 0;" :size="isMobile ? 'small' : ''">
@@ -825,7 +730,6 @@
{ title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
{ title: '{{ i18n "pages.xray.outbound.address"}}', align: 'center', width: 50, scopedSlots: { customRender: 'address' } },
{ title: '{{ i18n "pages.xray.dns.domains"}}', align: 'center', width: 50, scopedSlots: { customRender: 'domain' } },
{ title: '{{ i18n "pages.xray.dns.expectIPs"}}', align: 'center', width: 50, scopedSlots: { customRender: 'expectIPs' } },
];
const fakednsColumns = [
@@ -887,69 +791,57 @@
protocol: "freedom"
},
routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
log: {
loglevel: ["none", "debug", "info", "warning", "error"],
access: ["none", "./access.log"],
error: ["none", "./error.log"],
dnsLog: false,
maskAddress: ["quarter", "half", "full"],
},
logLevel: ["none" , "debug" , "info" , "warning", "error"],
access: [],
error: [],
settingsData: {
protocols: {
bittorrent: ["bittorrent"],
},
IPsOptions: [
{ label: 'Private IPs', value: 'geoip:private' },
{ label: '🇮🇷 Iran', value: 'ext:geoip_IR.dat:ir' },
{ label: '🇨🇳 China', value: 'geoip:cn' },
{ label: '🇷🇺 Russia', value: 'ext:geoip_RU.dat:ru' },
{ label: '🇻🇳 Vietnam', value: 'geoip:vn' },
{ label: '🇪🇸 Spain', value: 'geoip:es' },
{ label: '🇮🇩 Indonesia', value: 'geoip:id' },
{ label: '🇺🇦 Ukraine', value: 'geoip:ua' },
{ label: '🇹🇷 Türkiye', value: 'geoip:tr' },
{ label: '🇧🇷 Brazil', value: 'geoip:br' },
],
DomainsOptions: [
{ label: '🇮🇷 Iran', value: 'ext:geosite_IR.dat:ir' },
{ label: '🇮🇷 .ir', value: 'regexp:.*\\.ir$' },
{ label: '🇮🇷 .ایران', value: 'regexp:.*\\.xn--mgba3a4f16a$' },
{ label: '🇨🇳 China', value: 'geosite:cn' },
{ label: '🇨🇳 .cn', value: 'regexp:.*\\.cn$' },
{ label: '🇷🇺 Russia', value: 'ext:geosite_RU.dat:ru-available-only-inside' },
{ label: '🇷🇺 .ru', value: 'regexp:.*\\.ru$' },
{ label: '🇷🇺 .su', value: 'regexp:.*\\.su$' },
{ label: '🇷🇺 .рф', value: 'regexp:.*\\.xn--p1ai$' },
{ label: '🇻🇳 .vn', value: 'regexp:.*\\.vn$' },
],
BlockDomainsOptions: [
{ label: 'Ads All', value: 'geosite:category-ads-all' },
{ label: 'Ads IR 🇮🇷', value: 'ext:geosite_IR.dat:category-ads-all' },
{ label: 'Ads RU 🇷🇺', value: 'ext:geosite_RU.dat:category-ads-all' },
{ label: 'Malware 🇮🇷', value: 'ext:geosite_IR.dat:malware' },
{ label: 'Phishing 🇮🇷', value: 'ext:geosite_IR.dat:phishing' },
{ label: 'Cryptominers 🇮🇷', value: 'ext:geosite_IR.dat:cryptominers' },
{ label: '🇮🇷 Iran', value: 'ext:geosite_IR.dat:ir' },
{ label: '🇮🇷 .ir', value: 'regexp:.*\\.ir$' },
{ label: '🇮🇷 .ایران', value: 'regexp:.*\\.xn--mgba3a4f16a$' },
{ label: '🇨🇳 China', value: 'geosite:cn' },
{ label: '🇨🇳 .cn', value: 'regexp:.*\\.cn$' },
{ label: '🇷🇺 Russia', value: 'ext:geosite_RU.dat:ru-available-only-inside' },
{ label: '🇷🇺 .ru', value: 'regexp:.*\\.ru' },
{ label: '🇷🇺 .su', value: 'regexp:.*\\.su$' },
{ label: '🇷🇺 .рф', value: 'regexp:.*\\.xn--p1ai$' },
{ label: '🇻🇳 .vn', value: 'regexp:.*\\.vn$' },
],
ServicesOptions: [
{ label: 'Apple', value: 'geosite:apple' },
{ label: 'Meta', value: 'geosite:meta' },
{ label: 'Google', value: 'geosite:google' },
{ label: 'OpenAI', value: 'geosite:openai' },
{ label: 'Spotify', value: 'geosite:spotify' },
{ label: 'Netflix', value: 'geosite:netflix' },
{ label: 'Reddit', value: 'geosite:reddit' },
{ label: 'Speedtest', value: 'geosite:speedtest' },
],
ips: {
local: ["geoip:private"],
cn: ["geoip:cn"],
ir: ["ext:geoip_IR.dat:ir"],
ru: ["geoip:ru"],
vn: ["ext:geoip_VN.dat:vn"],
},
domains: {
ads: [
"geosite:category-ads-all",
"ext:geosite_IR.dat:category-ads-all"
],
security: [
"ext:geosite_IR.dat:malware",
"ext:geosite_IR.dat:phishing",
"ext:geosite_IR.dat:cryptominers"
],
speedtest: ["geosite:speedtest"],
openai: ["geosite:openai"],
google: ["geosite:google"],
spotify: ["geosite:spotify"],
netflix: ["geosite:netflix"],
meta: ["geosite:meta"],
apple: ["geosite:apple"],
reddit: ["geosite:reddit"],
cn: [
"geosite:cn",
"regexp:.*\\.cn$"
],
ru: [
"geosite:category-gov-ru",
"regexp:.*\\.ru$"
],
ir: [
"regexp:.*\\.ir$",
"regexp:.*\\.xn--mgba3a4f16a$", // .ایران
"ext:geosite_IR.dat:ir"
],
vn: [
"regexp:.*\\.vn$",
"ext:geosite_VN.dat:vn",
"ext:geosite_VN.dat:ads"
]
},
familyProtectDNS: {
"servers": [
"1.1.1.3", // https://developers.cloudflare.com/1.1.1.1/setup/
@@ -1627,11 +1519,27 @@
templateSettings: {
get: function () {
const parsedSettings = this.xraySetting ? JSON.parse(this.xraySetting) : null;
let accessLogPath = "./access.log";
let errorLogPath = "./error.log";
if (parsedSettings && parsedSettings.log) {
if (parsedSettings.log.access && parsedSettings.log.access !== "none") {
accessLogPath = parsedSettings.log.access;
}
if (parsedSettings.log.error && parsedSettings.log.error !== "none") {
errorLogPath = parsedSettings.log.error;
}
}
this.access = ["none", accessLogPath];
this.error = ["none", errorLogPath];
return parsedSettings;
},
set: function (newValue) {
if (newValue) {
if (newValue && newValue.log) {
this.xraySetting = JSON.stringify(newValue, null, 2);
this.access = ["none", newValue.log.access || "./access.log"];
this.error = ["none", newValue.log.error || "./error.log"];
}
},
},
@@ -1780,7 +1688,7 @@
this.templateSettings = newTemplateSettings;
}
},
logLevel: {
setLogLevel: {
get: function () {
if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.loglevel) return "warning";
return this.templateSettings.log.loglevel;
@@ -1813,40 +1721,6 @@
this.templateSettings = newTemplateSettings;
}
},
dnslog: {
get: function () {
if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.dnsLog) return false;
return this.templateSettings.log.dnsLog;
},
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.log.dnsLog = newValue;
this.templateSettings = newTemplateSettings;
}
},
outboundTraffic: {
get: function () {
if (!this.templateSettings || !this.templateSettings.policy.system || !this.templateSettings.policy.system.statsOutboundDownlink) return false;
return this.templateSettings.policy.system.statsOutboundDownlink;
},
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.policy.system.statsOutboundDownlink = newValue;
newTemplateSettings.policy.system.statsOutboundUplink = newValue;
this.templateSettings = newTemplateSettings;
}
},
maskAddressLog: {
get: function () {
if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.maskAddress) return "";
return this.templateSettings.log.maskAddress;
},
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.log.maskAddress = newValue;
this.templateSettings = newTemplateSettings;
}
},
blockedIPs: {
get: function () {
return this.templateRuleGetter({ outboundTag: "blocked", property: "ip" });
@@ -1918,6 +1792,54 @@
}
},
},
privateIpSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.local, this.blockedIPs);
},
set: function (newValue) {
if (newValue) {
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.local];
} else {
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.local.includes(data));
}
},
},
AdsSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.ads, this.blockedDomains);
},
set: function (newValue) {
if (newValue) {
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ads];
} else {
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ads.includes(data));
}
},
},
SecuritySettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.security, this.blockedDomains);
},
set: function (newValue) {
if (newValue) {
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.security];
} else {
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.security.includes(data));
}
},
},
SpeedTestSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.speedtest, this.blockedDomains);
},
set: function (newValue) {
if (newValue) {
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.speedtest];
} else {
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.speedtest.includes(data));
}
},
},
familyProtectSettings: {
get: function () {
if (!this.templateSettings || !this.templateSettings.dns || !this.templateSettings.dns.servers) return false;
@@ -1933,11 +1855,311 @@
this.templateSettings = newTemplateSettings;
},
},
GoogleIPv4Settings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.google, this.ipv4Domains);
},
set: function (newValue) {
if (newValue) {
this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.google];
} else {
this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.google.includes(data));
}
},
},
NetflixIPv4Settings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.netflix, this.ipv4Domains);
},
set: function (newValue) {
if (newValue) {
this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.netflix];
} else {
this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.netflix.includes(data));
}
},
},
IRIpSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.ir, this.blockedIPs);
},
set: function (newValue) {
if (newValue) {
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.ir];
} else {
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.ir.includes(data));
}
}
},
IRDomainSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.ir, this.blockedDomains);
},
set: function (newValue) {
if (newValue) {
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ir];
} else {
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ir.includes(data));
}
}
},
ChinaIpSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.cn, this.blockedIPs);
},
set: function (newValue) {
if (newValue) {
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.cn];
} else {
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.cn.includes(data));
}
}
},
ChinaDomainSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.cn, this.blockedDomains);
},
set: function (newValue) {
if (newValue) {
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.cn];
} else {
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.cn.includes(data));
}
}
},
RussiaIpSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.ru, this.blockedIPs);
},
set: function (newValue) {
if (newValue) {
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.ru];
} else {
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.ru.includes(data));
}
}
},
RussiaDomainSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.ru, this.blockedDomains);
},
set: function (newValue) {
if (newValue) {
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ru];
} else {
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ru.includes(data));
}
}
},
VNIpSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.vn, this.blockedIPs);
},
set: function (newValue) {
if (newValue) {
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.vn];
} else {
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.vn.includes(data));
}
}
},
VNDomainSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.vn, this.blockedDomains);
},
set: function (newValue) {
if (newValue) {
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.vn];
} else {
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.vn.includes(data));
}
}
},
IRIpDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.ir, this.directIPs);
},
set: function (newValue) {
if (newValue) {
this.directIPs = [...this.directIPs, ...this.settingsData.ips.ir];
} else {
this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.ir.includes(data));
}
}
},
IRDomainDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.ir, this.directDomains);
},
set: function (newValue) {
if (newValue) {
this.directDomains = [...this.directDomains, ...this.settingsData.domains.ir];
} else {
this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.ir.includes(data));
}
}
},
ChinaIpDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.cn, this.directIPs);
},
set: function (newValue) {
if (newValue) {
this.directIPs = [...this.directIPs, ...this.settingsData.ips.cn];
} else {
this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.cn.includes(data));
}
}
},
ChinaDomainDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.cn, this.directDomains);
},
set: function (newValue) {
if (newValue) {
this.directDomains = [...this.directDomains, ...this.settingsData.domains.cn];
} else {
this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.cn.includes(data));
}
}
},
RussiaIpDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.ru, this.directIPs);
},
set: function (newValue) {
if (newValue) {
this.directIPs = [...this.directIPs, ...this.settingsData.ips.ru];
} else {
this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.ru.includes(data));
}
}
},
RussiaDomainDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.ru, this.directDomains);
},
set: function (newValue) {
if (newValue) {
this.directDomains = [...this.directDomains, ...this.settingsData.domains.ru];
} else {
this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.ru.includes(data));
}
}
},
VNIpDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.vn, this.directIPs);
},
set: function (newValue) {
if (newValue) {
this.directIPs = [...this.directIPs, ...this.settingsData.ips.vn];
} else {
this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.vn.includes(data));
}
}
},
VNDomainDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.vn, this.directDomains);
},
set: function (newValue) {
if (newValue) {
this.directDomains = [...this.directDomains, ...this.settingsData.domains.vn];
} else {
this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.vn.includes(data));
}
}
},
WarpExist: {
get: function() {
return this.templateSettings ? this.templateSettings.outbounds.findIndex((o) => o.tag == "warp")>=0 : false;
},
},
GoogleWARPSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.google, this.warpDomains);
},
set: function (newValue) {
if (newValue) {
this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.google];
} else {
this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.google.includes(data));
}
},
},
OpenAIWARPSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.openai, this.warpDomains);
},
set: function (newValue) {
if (newValue) {
this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.openai];
} else {
this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.openai.includes(data));
}
},
},
NetflixWARPSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.netflix, this.warpDomains);
},
set: function (newValue) {
if (newValue) {
this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.netflix];
} else {
this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.netflix.includes(data));
}
},
},
MetaWARPSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.meta, this.warpDomains);
},
set: function (newValue) {
if (newValue) {
this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.meta];
} else {
this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.meta.includes(data));
}
},
},
AppleWARPSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.apple, this.warpDomains);
},
set: function (newValue) {
if (newValue) {
this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.apple];
} else {
this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.apple.includes(data));
}
},
},
RedditWARPSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.reddit, this.warpDomains);
},
set: function (newValue) {
if (newValue) {
this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.reddit];
} else {
this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.reddit.includes(data));
}
},
},
SpotifyWARPSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.spotify, this.warpDomains);
},
set: function (newValue) {
if (newValue) {
this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.spotify];
} else {
this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.spotify.includes(data));
}
},
},
enableDNS: {
get: function () {
return this.templateSettings ? this.templateSettings.dns != null : false;

View File

@@ -9,6 +9,7 @@ import (
"os/exec"
"regexp"
"sort"
"strings"
"time"
"x-ui/database"
@@ -35,21 +36,20 @@ func (j *CheckClientIpJob) Run() {
}
shouldClearAccessLog := false
iplimitActive := j.hasLimitIp()
f2bInstalled := j.checkFail2BanInstalled()
isAccessLogAvailable := j.checkAccessLogAvailable(iplimitActive)
isAccessLogAvailable := j.checkAccessLogAvailable(f2bInstalled)
if iplimitActive {
if j.hasLimitIp() {
if f2bInstalled && isAccessLogAvailable {
shouldClearAccessLog = j.processLogFile()
} else {
if !f2bInstalled {
logger.Warning("[LimitIP] Fail2Ban is not installed, Please install Fail2Ban from the x-ui bash menu.")
logger.Warning("[iplimit] fail2ban is not installed. IP limiting may not work properly.")
}
}
}
if shouldClearAccessLog || (isAccessLogAvailable && time.Now().Unix()-j.lastClear > 3600) {
if shouldClearAccessLog || isAccessLogAvailable && time.Now().Unix()-j.lastClear > 3600 {
j.clearAccessLog()
}
}
@@ -58,18 +58,23 @@ func (j *CheckClientIpJob) clearAccessLog() {
logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
j.checkError(err)
// get access log path to open it
accessLogPath, err := xray.GetAccessLogPath()
j.checkError(err)
// reopen the access log file for reading
file, err := os.Open(accessLogPath)
j.checkError(err)
// copy access log content to persistent file
_, err = io.Copy(logAccessP, file)
j.checkError(err)
// close the file after copying content
logAccessP.Close()
file.Close()
// clean access log
err = os.Truncate(accessLogPath, 0)
j.checkError(err)
j.lastClear = time.Now().Unix()
@@ -105,59 +110,58 @@ func (j *CheckClientIpJob) hasLimitIp() bool {
}
func (j *CheckClientIpJob) processLogFile() bool {
accessLogPath, err := xray.GetAccessLogPath()
j.checkError(err)
ipRegex := regexp.MustCompile(`from (?:tcp:|udp:)?\[?([0-9a-fA-F\.:]+)\]?:\d+ accepted`)
emailRegex := regexp.MustCompile(`email: (.+)$`)
file, err := os.Open(accessLogPath)
j.checkError(err)
accessLogPath, _ := xray.GetAccessLogPath()
file, _ := os.Open(accessLogPath)
defer file.Close()
inboundClientIps := make(map[string]map[string]struct{}, 100)
InboundClientIps := make(map[string][]string)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
ipMatches := ipRegex.FindStringSubmatch(line)
if len(ipMatches) < 2 {
continue
}
ipRegx, _ := regexp.Compile(`(\d+\.\d+\.\d+\.\d+).* accepted`)
emailRegx, _ := regexp.Compile(`email:.+`)
ip := ipMatches[1]
matches := ipRegx.FindStringSubmatch(line)
if len(matches) > 1 {
ip := matches[1]
if ip == "127.0.0.1" {
continue
}
if ip == "127.0.0.1" || ip == "::1" {
continue
}
matchesEmail := emailRegx.FindString(line)
if matchesEmail == "" {
continue
}
matchesEmail = strings.TrimSpace(strings.Split(matchesEmail, "email: ")[1])
emailMatches := emailRegex.FindStringSubmatch(line)
if len(emailMatches) < 2 {
continue
if InboundClientIps[matchesEmail] != nil {
if j.contains(InboundClientIps[matchesEmail], ip) {
continue
}
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
} else {
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
}
}
email := emailMatches[1]
if _, exists := inboundClientIps[email]; !exists {
inboundClientIps[email] = make(map[string]struct{})
}
inboundClientIps[email][ip] = struct{}{}
}
j.checkError(scanner.Err())
file.Close()
shouldCleanLog := false
for email, uniqueIps := range inboundClientIps {
ips := make([]string, 0, len(uniqueIps))
for ip := range uniqueIps {
ips = append(ips, ip)
}
for clientEmail, ips := range InboundClientIps {
inboundClientIps, err := j.getInboundClientIps(clientEmail)
sort.Strings(ips)
clientIpsRecord, err := j.getInboundClientIps(email)
if err != nil {
j.addInboundClientIps(email, ips)
continue
j.addInboundClientIps(clientEmail, ips)
} else {
shouldCleanLog = j.updateInboundClientIps(inboundClientIps, clientEmail, ips)
}
shouldCleanLog = j.updateInboundClientIps(clientIpsRecord, email, ips) || shouldCleanLog
}
return shouldCleanLog
@@ -170,20 +174,28 @@ func (j *CheckClientIpJob) checkFail2BanInstalled() bool {
return err == nil
}
func (j *CheckClientIpJob) checkAccessLogAvailable(iplimitActive bool) bool {
func (j *CheckClientIpJob) checkAccessLogAvailable(handleWarning bool) bool {
isAvailable := true
warningMsg := ""
accessLogPath, err := xray.GetAccessLogPath()
if err != nil {
return false
}
if accessLogPath == "none" || accessLogPath == "" {
if iplimitActive {
logger.Warning("[LimitIP] Access log path is not set, Please configure the access log path in Xray configs.")
}
return false
// access log is not available if it is set to 'none' or an empty string
switch accessLogPath {
case "none":
warningMsg = "Access log is set to 'none', check your Xray Configs"
isAvailable = false
case "":
warningMsg = "Access log doesn't exist in your Xray Configs"
isAvailable = false
}
return true
if handleWarning && warningMsg != "" {
logger.Warning(warningMsg)
}
return isAvailable
}
func (j *CheckClientIpJob) checkError(e error) {
@@ -248,6 +260,7 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
inboundClientIps.ClientEmail = clientEmail
inboundClientIps.Ips = string(jsonIps)
// Fetch inbound settings by client email
inbound, err := j.getInboundByEmail(clientEmail)
if err != nil {
logger.Errorf("failed to fetch inbound settings for email %s: %s", clientEmail, err)
@@ -259,12 +272,14 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
return false
}
// Unmarshal settings to get client limits
settings := map[string][]model.Client{}
json.Unmarshal([]byte(inbound.Settings), &settings)
clients := settings["clients"]
shouldCleanLog := false
j.disAllowedIps = []string{}
// Open log file for IP limits
logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
logger.Errorf("failed to open IP limit log file: %s", err)
@@ -274,6 +289,7 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
log.SetOutput(logIpFile)
log.SetFlags(log.LstdFlags)
// Check client IP limits
for _, client := range clients {
if client.Email == clientEmail {
limitIp := client.LimitIP
@@ -309,12 +325,12 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) {
db := database.GetDB()
inbound := &model.Inbound{}
var inbounds *model.Inbound
err := db.Model(&model.Inbound{}).Where("settings LIKE ?", "%"+clientEmail+"%").First(inbound).Error
err := db.Model(model.Inbound{}).Where("settings LIKE ?", "%"+clientEmail+"%").Find(&inbounds).Error
if err != nil {
return nil, err
}
return inbound, nil
return inbounds, nil
}

Some files were not shown because too many files have changed in this diff Show More