Compare commits

..

No commits in common. "main" and "v2.8.6" have entirely different histories.
main ... v2.8.6

82 changed files with 1759 additions and 4842 deletions

View file

@ -1,155 +0,0 @@
# 3X-UI Development Guide
## Project Overview
3X-UI is a web-based control panel for managing Xray-core servers. It's a Go application using Gin web framework with embedded static assets and SQLite database. The panel manages VPN/proxy inbounds, monitors traffic, and provides Telegram bot integration.
## Architecture
### Core Components
- **main.go**: Entry point that initializes database, web server, and subscription server. Handles graceful shutdown via SIGHUP/SIGTERM signals
- **web/**: Primary web server with Gin router, HTML templates, and static assets embedded via `//go:embed`
- **xray/**: Xray-core process management and API communication for traffic monitoring
- **database/**: GORM-based SQLite database with models in `database/model/`
- **sub/**: Subscription server running alongside main web server (separate port)
- **web/service/**: Business logic layer containing InboundService, SettingService, TgBot, etc.
- **web/controller/**: HTTP handlers using Gin context (`*gin.Context`)
- **web/job/**: Cron-based background jobs for traffic monitoring, CPU checks, LDAP sync
### Key Architectural Patterns
1. **Embedded Resources**: All web assets (HTML, CSS, JS, translations) are embedded at compile time using `embed.FS`:
- `web/assets``assetsFS`
- `web/html``htmlFS`
- `web/translation``i18nFS`
2. **Dual Server Design**: Main web panel + subscription server run concurrently, managed by `web/global` package
3. **Xray Integration**: Panel generates `config.json` for Xray binary, communicates via gRPC API for real-time traffic stats
4. **Signal-Based Restart**: SIGHUP triggers graceful restart. **Critical**: Always call `service.StopBot()` before restart to prevent Telegram bot 409 conflicts
5. **Database Seeders**: Uses `HistoryOfSeeders` model to track one-time migrations (e.g., password bcrypt migration)
## Development Workflows
### Building & Running
```bash
# Build (creates bin/3x-ui.exe)
go run tasks.json → "go: build" task
# Run with debug logging
XUI_DEBUG=true go run ./main.go
# Or use task: "go: run"
# Test
go test ./...
```
### Command-Line Operations
The main.go accepts flags for admin tasks:
- `-reset` - Reset all panel settings to defaults
- `-show` - Display current settings (port, paths)
- Use these by running the binary directly, not via web interface
### Database Management
- DB path: Configured via `config.GetDBPath()`, typically `/etc/x-ui/x-ui.db`
- Models: Located in `database/model/model.go` - Auto-migrated on startup
- Seeders: Use `HistoryOfSeeders` to prevent re-running migrations
- Default credentials: admin/admin (hashed with bcrypt)
### Telegram Bot Development
- Bot instance in `web/service/tgbot.go` (3700+ lines)
- Uses `telego` library with long polling
- **Critical Pattern**: Must call `service.StopBot()` before any server restart to prevent 409 bot conflicts
- Bot handlers use `telegohandler.BotHandler` for routing
- i18n via embedded `i18nFS` passed to bot startup
## Code Conventions
### Service Layer Pattern
Services inject dependencies (like xray.XrayAPI) and operate on GORM models:
```go
type InboundService struct {
xrayApi xray.XrayAPI
}
func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
// Business logic here
}
```
### Controller Pattern
Controllers use Gin context and inherit from BaseController:
```go
func (a *InboundController) getInbounds(c *gin.Context) {
// Use I18nWeb(c, "key") for translations
// Check auth via checkLogin middleware
}
```
### Configuration Management
- Environment vars: `XUI_DEBUG`, `XUI_LOG_LEVEL`, `XUI_MAIN_FOLDER`
- Config embedded files: `config/version`, `config/name`
- Use `config.GetLogLevel()`, `config.GetDBPath()` helpers
### Internationalization
- Translation files: `web/translation/translate.*.toml`
- Access via `I18nWeb(c, "pages.login.loginAgain")` in controllers
- Use `locale.I18nType` enum (Web, Api, etc.)
## External Dependencies & Integration
### Xray-core
- Binary management: Download platform-specific binary (`xray-{os}-{arch}`) to bin folder
- Config generation: Panel creates `config.json` dynamically from inbound/outbound settings
- Process control: Start/stop via `xray/process.go`
- gRPC API: Real-time stats via `xray/api.go` using `google.golang.org/grpc`
### Critical External Paths
- Xray binary: `{bin_folder}/xray-{os}-{arch}`
- Xray config: `{bin_folder}/config.json`
- GeoIP/GeoSite: `{bin_folder}/geoip.dat`, `geosite.dat`
- Logs: `{log_folder}/3xipl.log`, `3xipl-banned.log`
### Job Scheduling
Uses `robfig/cron/v3` for periodic tasks:
- Traffic monitoring: `xray_traffic_job.go`
- CPU alerts: `check_cpu_usage.go`
- IP tracking: `check_client_ip_job.go`
- LDAP sync: `ldap_sync_job.go`
Jobs registered in `web/web.go` during server initialization
## Deployment & Scripts
### Installation Script Pattern
Both `install.sh` and `x-ui.sh` follow these patterns:
- Multi-distro support via `$release` variable (ubuntu, debian, centos, arch, etc.)
- Port detection with `is_port_in_use()` using ss/netstat/lsof
- Systemd service management with distro-specific unit files (`.service.debian`, `.service.arch`, `.service.rhel`)
### Docker Build
Multi-stage Dockerfile:
1. **Builder**: CGO-enabled build, runs `DockerInit.sh` to download Xray binary
2. **Final**: Alpine-based with fail2ban pre-configured
### Key File Locations (Production)
- Binary: `/usr/local/x-ui/`
- Database: `/etc/x-ui/x-ui.db`
- Logs: `/var/log/x-ui/`
- Service: `/etc/systemd/system/x-ui.service.*`
## Testing & Debugging
- Set `XUI_DEBUG=true` for detailed logging
- Check Xray process: `x-ui.sh` script provides menu for status/logs
- Database inspection: Direct SQLite access to x-ui.db
- Traffic debugging: Check `3xipl.log` for IP limit tracking
- Telegram bot: Logs show bot initialization and command handling
## Common Gotchas
1. **Bot Restart**: Always stop Telegram bot before server restart to avoid 409 conflict
2. **Embedded Assets**: Changes to HTML/CSS require recompilation (not hot-reload)
3. **Password Migration**: Seeder system tracks bcrypt migration - check `HistoryOfSeeders` table
4. **Port Binding**: Subscription server uses different port from main panel
5. **Xray Binary**: Must match OS/arch exactly - managed by installer scripts
6. **Session Management**: Uses `gin-contrib/sessions` with cookie store
7. **IP Limitation**: Implements "last IP wins" - when client exceeds LimitIP, oldest connections are automatically disconnected via Xray API to allow newest IPs

View file

@ -1,31 +0,0 @@
name: Cleanup Caches
on:
schedule:
- cron: '0 3 * * 0' # every Sunday
workflow_dispatch:
jobs:
cleanup:
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Delete caches older than 3 days
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
CUTOFF_DATE=$(date -d "3 days ago" -Ins --utc | sed 's/+0000/Z/')
echo "Deleting caches older than: $CUTOFF_DATE"
CACHE_IDS=$(gh api --paginate repos/${{ github.repository }}/actions/caches \
--jq ".actions_caches[] | select(.last_accessed_at < \"$CUTOFF_DATE\") | .id" 2>/dev/null)
if [ -z "$CACHE_IDS" ]; then
echo "No old caches found to delete."
else
echo "$CACHE_IDS" | while read CACHE_ID; do
echo "Deleting cache: $CACHE_ID"
gh api -X DELETE repos/${{ github.repository }}/actions/caches/$CACHE_ID
done
echo "Old caches deleted successfully."
fi

View file

@ -18,7 +18,6 @@ on:
- 'go.mod' - 'go.mod'
- 'go.sum' - 'go.sum'
- 'x-ui.service.debian' - 'x-ui.service.debian'
- 'x-ui.service.arch'
- 'x-ui.service.rhel' - 'x-ui.service.rhel'
jobs: jobs:
@ -81,7 +80,6 @@ jobs:
mkdir x-ui mkdir x-ui
cp xui-release x-ui/ cp xui-release x-ui/
cp x-ui.service.debian x-ui/ cp x-ui.service.debian x-ui/
cp x-ui.service.arch x-ui/
cp x-ui.service.rhel x-ui/ cp x-ui.service.rhel x-ui/
cp x-ui.sh x-ui/ cp x-ui.sh x-ui/
mv x-ui/xui-release x-ui/x-ui mv x-ui/xui-release x-ui/x-ui
@ -89,7 +87,7 @@ jobs:
cd x-ui/bin cd x-ui/bin
# Download dependencies # Download dependencies
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v26.2.6/" Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.12.8/"
if [ "${{ matrix.platform }}" == "amd64" ]; then if [ "${{ matrix.platform }}" == "amd64" ]; then
wget -q ${Xray_URL}Xray-linux-64.zip wget -q ${Xray_URL}Xray-linux-64.zip
unzip Xray-linux-64.zip unzip Xray-linux-64.zip
@ -173,42 +171,21 @@ jobs:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
- name: Install MSYS2 - name: Build 3X-UI for Windows
uses: msys2/setup-msys2@v2
with:
msystem: MINGW64
update: true
install: >-
mingw-w64-x86_64-gcc
mingw-w64-x86_64-sqlite3
mingw-w64-x86_64-pkg-config
- name: Build 3X-UI for Windows (CGO)
shell: msys2 {0}
run: |
export PATH="/c/hostedtoolcache/windows/go/$(ls /c/hostedtoolcache/windows/go | sort -V | tail -n1)/x64/bin:$PATH"
export CGO_ENABLED=1
export GOOS=windows
export GOARCH=amd64
export CC=x86_64-w64-mingw32-gcc
which go
go version
gcc --version
go build -ldflags "-w -s" -o xui-release.exe -v main.go
- name: Copy and download resources
shell: pwsh shell: pwsh
run: | run: |
$env:CGO_ENABLED="1"
$env:GOOS="windows"
$env:GOARCH="amd64"
go build -ldflags "-w -s" -o xui-release.exe -v main.go
mkdir x-ui mkdir x-ui
Copy-Item xui-release.exe x-ui\x-ui.exe Copy-Item xui-release.exe x-ui\
mkdir x-ui\bin mkdir x-ui\bin
cd x-ui\bin cd x-ui\bin
# Download Xray for Windows # Download Xray for Windows
$Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v26.2.6/" $Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.12.8/"
Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip" Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath . Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
Remove-Item "Xray-windows-64.zip" Remove-Item "Xray-windows-64.zip"

View file

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

View file

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

View file

@ -1 +1 @@
2.8.10 2.8.6

View file

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

58
go.mod
View file

@ -1,6 +1,6 @@
module github.com/mhsanaei/3x-ui/v2 module github.com/mhsanaei/3x-ui/v2
go 1.25.7 go 1.25.5
require ( require (
github.com/gin-contrib/gzip v1.2.5 github.com/gin-contrib/gzip v1.2.5
@ -11,20 +11,20 @@ require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mymmrac/telego v1.6.0 github.com/mymmrac/telego v1.4.0
github.com/nicksnyder/go-i18n/v2 v2.6.1 github.com/nicksnyder/go-i18n/v2 v2.6.1
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.2.4 github.com/pelletier/go-toml/v2 v2.2.4
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.26.1 github.com/shirou/gopsutil/v4 v4.25.12
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/valyala/fasthttp v1.69.0 github.com/valyala/fasthttp v1.68.0
github.com/xlzd/gotp v0.1.0 github.com/xlzd/gotp v0.1.0
github.com/xtls/xray-core v1.260206.0 github.com/xtls/xray-core v1.251208.0
go.uber.org/atomic v1.11.0 go.uber.org/atomic v1.11.0
golang.org/x/crypto v0.48.0 golang.org/x/crypto v0.46.0
golang.org/x/sys v0.41.0 golang.org/x/sys v0.39.0
golang.org/x/text v0.34.0 golang.org/x/text v0.32.0
google.golang.org/grpc v1.78.0 google.golang.org/grpc v1.78.0
gorm.io/driver/sqlite v1.6.0 gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1 gorm.io/gorm v1.31.1
@ -33,21 +33,21 @@ require (
require ( require (
github.com/Azure/go-ntlmssp v0.1.0 // indirect github.com/Azure/go-ntlmssp v0.1.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect
github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudflare/circl v1.6.2 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
github.com/ebitengine/purego v0.9.1 // indirect github.com/ebitengine/purego v0.9.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-ole/go-ole v1.3.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/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect github.com/goccy/go-yaml v1.19.1 // indirect
github.com/google/btree v1.1.3 // indirect github.com/google/btree v1.1.3 // indirect
github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
@ -57,27 +57,30 @@ require (
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/juju/ratelimit v1.0.2 // indirect github.com/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.34 // indirect github.com/mattn/go-sqlite3 v1.14.33 // indirect
github.com/miekg/dns v1.1.72 // indirect github.com/miekg/dns v1.1.69 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pires/go-proxyproto v0.11.0 // indirect github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect github.com/quic-go/quic-go v0.58.0 // indirect
github.com/refraction-networking/utls v1.8.2 // indirect github.com/refraction-networking/utls v1.8.1 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagernet/sing v0.7.18 // indirect github.com/sagernet/sing v0.7.14 // indirect
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect github.com/tklauser/numcpus v0.11.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fastjson v1.6.7 // indirect github.com/valyala/fastjson v1.6.7 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect github.com/vishvananda/netlink v1.3.1 // indirect
@ -85,17 +88,16 @@ require (
github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 // indirect github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.24.0 // indirect golang.org/x/arch v0.23.0 // indirect
golang.org/x/exp v0.0.0-20260209203927-2842357ff358 // indirect golang.org/x/mod v0.31.0 // indirect
golang.org/x/mod v0.33.0 // indirect golang.org/x/net v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/time v0.14.0 // indirect golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.42.0 // indirect golang.org/x/tools v0.40.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
lukechampine.com/blake3 v1.4.1 // indirect lukechampine.com/blake3 v1.4.1 // indirect
) )

115
go.sum
View file

@ -6,25 +6,26 @@ github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktp
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178 h1:bSq8n+gX4oO/qnM3MKf4kroW75n+phO9Qp6nigJKZ1E=
github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178/go.mod h1:N1WIjPphkqs4efXWuyDNQ6OjjIK04vM3h+bEgwV+eVU=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM=
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
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 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI= github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI=
@ -56,8 +57,8 @@ github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy0
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@ -105,8 +106,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -119,17 +120,17 @@ github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIi
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 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-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v1.6.0 h1:Zc8rgyHozvd/7ZgyrigyHdAF9koHYMfilYfyB6wlFC0= github.com/mymmrac/telego v1.4.0 h1:z74W5lfOTgLplQXuZPjDsRvvvI0iQatO2gp/XZz7s3I=
github.com/mymmrac/telego v1.6.0/go.mod h1:xt6ZWA8zi8KmuzryE1ImEdl9JSwjHNpM4yhC7D8hU4Y= github.com/mymmrac/telego v1.4.0/go.mod h1:u9fKXZSOCOdMj6K0U69fQqeAvDE+2RGkHKkDksijp3o=
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ= github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA= github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
@ -138,28 +139,32 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/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 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo= github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo=
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= 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/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sagernet/sing v0.7.18 h1:iZHkaru1/MoHugx3G+9S3WG4owMewKO/KvieE2Pzk4E= github.com/sagernet/sing v0.7.14 h1:5QQRDCUvYNOMyVp3LuK/hYEBAIv0VsbD3x/l9zH467s=
github.com/sagernet/sing v0.7.18/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.7.14/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM= github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8= github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo= github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc= github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -167,6 +172,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
@ -181,10 +187,12 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM= github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM=
github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
@ -195,8 +203,8 @@ github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg= github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 h1:UXjrmniKlY+ZbIqpN91lejB3pszQQQRVu1vqH/p/aGM= github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 h1:UXjrmniKlY+ZbIqpN91lejB3pszQQQRVu1vqH/p/aGM=
github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237/go.mod h1:vbHCV/3VWUvy1oKvTxxWJRPEWSeR1sYgQHIh6u/JiZQ= github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237/go.mod h1:vbHCV/3VWUvy1oKvTxxWJRPEWSeR1sYgQHIh6u/JiZQ=
github.com/xtls/xray-core v1.260206.0 h1:gY8IV6u76CW93txL9QmacgZ0Udxr2Q3e9qUxXAhdHqI= github.com/xtls/xray-core v1.251208.0 h1:9jIXi+9KXnfmT5esSYNf9VAQlQkaAP8bG413B0eyAes=
github.com/xtls/xray-core v1.260206.0/go.mod h1:GyFIgVGRJkt3eyV/NMcdxOKXcJPqGGpyupHzy16uJhU= github.com/xtls/xray-core v1.251208.0/go.mod h1:kclzboEF0g6VBrp9/NXm8C0Aj64SDBt52OfthH1LSr4=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
@ -221,16 +229,14 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= 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= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y= golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20260209203927-2842357ff358 h1:kpfSV7uLwKJbFSEgNhWzGSL47NDSF/5pYYQw1V0ub6c= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/exp v0.0.0-20260209203927-2842357ff358/go.mod h1:R3t0oliuryB5eenPWl3rrQxwnNM3WTwnsRZZiXLAAW8= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -239,22 +245,22 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
@ -265,13 +271,14 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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-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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h1:Lk6hARj5UPY47dBep70OD/TIMwikJ5fGUGX0Rm3Xigk= gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q= gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=

View file

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

View file

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

View file

@ -3,7 +3,6 @@ package sub
import ( import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"strconv"
"strings" "strings"
"github.com/mhsanaei/3x-ui/v2/config" "github.com/mhsanaei/3x-ui/v2/config"
@ -14,11 +13,6 @@ import (
// SUBController handles HTTP requests for subscription links and JSON configurations. // SUBController handles HTTP requests for subscription links and JSON configurations.
type SUBController struct { type SUBController struct {
subTitle string subTitle string
subSupportUrl string
subProfileUrl string
subAnnounce string
subEnableRouting bool
subRoutingRules string
subPath string subPath string
subJsonPath string subJsonPath string
jsonEnabled bool jsonEnabled bool
@ -44,20 +38,10 @@ func NewSUBController(
jsonMux string, jsonMux string,
jsonRules string, jsonRules string,
subTitle string, subTitle string,
subSupportUrl string,
subProfileUrl string,
subAnnounce string,
subEnableRouting bool,
subRoutingRules string,
) *SUBController { ) *SUBController {
sub := NewSubService(showInfo, rModel) sub := NewSubService(showInfo, rModel)
a := &SUBController{ a := &SUBController{
subTitle: subTitle, subTitle: subTitle,
subSupportUrl: subSupportUrl,
subProfileUrl: subProfileUrl,
subAnnounce: subAnnounce,
subEnableRouting: subEnableRouting,
subRoutingRules: subRoutingRules,
subPath: subPath, subPath: subPath,
subJsonPath: jsonPath, subJsonPath: jsonPath,
jsonEnabled: jsonEnabled, jsonEnabled: jsonEnabled,
@ -143,11 +127,7 @@ func (a *SUBController) subs(c *gin.Context) {
// Add headers // Add headers
header := fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000) header := fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
profileUrl := a.subProfileUrl a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle)
if profileUrl == "" {
profileUrl = fmt.Sprintf("%s://%s%s", scheme, hostWithPort, c.Request.RequestURI)
}
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle, a.subSupportUrl, profileUrl, a.subAnnounce, a.subEnableRouting, a.subRoutingRules)
if a.subEncrypt { if a.subEncrypt {
c.String(200, base64.StdEncoding.EncodeToString([]byte(result))) c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
@ -160,54 +140,22 @@ func (a *SUBController) subs(c *gin.Context) {
// subJsons handles HTTP requests for JSON subscription configurations. // subJsons handles HTTP requests for JSON subscription configurations.
func (a *SUBController) subJsons(c *gin.Context) { func (a *SUBController) subJsons(c *gin.Context) {
subId := c.Param("subid") subId := c.Param("subid")
scheme, host, hostWithPort, _ := a.subService.ResolveRequest(c) _, host, _, _ := a.subService.ResolveRequest(c)
jsonSub, header, err := a.subJsonService.GetJson(subId, host) jsonSub, header, err := a.subJsonService.GetJson(subId, host)
if err != nil || len(jsonSub) == 0 { if err != nil || len(jsonSub) == 0 {
c.String(400, "Error!") c.String(400, "Error!")
} else { } else {
// Add headers // Add headers
profileUrl := a.subProfileUrl a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle)
if profileUrl == "" {
profileUrl = fmt.Sprintf("%s://%s%s", scheme, hostWithPort, c.Request.RequestURI)
}
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle, a.subSupportUrl, profileUrl, a.subAnnounce, a.subEnableRouting, a.subRoutingRules)
c.String(200, jsonSub) c.String(200, jsonSub)
} }
} }
// ApplyCommonHeaders sets common HTTP headers for subscription responses including user info, update interval, and profile title. // ApplyCommonHeaders sets common HTTP headers for subscription responses including user info, update interval, and profile title.
func (a *SUBController) ApplyCommonHeaders( func (a *SUBController) ApplyCommonHeaders(c *gin.Context, header, updateInterval, profileTitle string) {
c *gin.Context,
header,
updateInterval,
profileTitle string,
profileSupportUrl string,
profileUrl string,
profileAnnounce string,
profileEnableRouting bool,
profileRoutingRules string,
) {
c.Writer.Header().Set("Subscription-Userinfo", header) c.Writer.Header().Set("Subscription-Userinfo", header)
c.Writer.Header().Set("Profile-Update-Interval", updateInterval) c.Writer.Header().Set("Profile-Update-Interval", updateInterval)
//Basics
if profileTitle != "" {
c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileTitle))) c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileTitle)))
} }
if profileSupportUrl != "" {
c.Writer.Header().Set("Support-Url", profileSupportUrl)
}
if profileUrl != "" {
c.Writer.Header().Set("Profile-Web-Page-Url", profileUrl)
}
if profileAnnounce != "" {
c.Writer.Header().Set("Announce", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileAnnounce)))
}
//Advanced (Happ)
c.Writer.Header().Set("Routing-Enable", strconv.FormatBool(profileEnableRouting))
if profileRoutingRules != "" {
c.Writer.Header().Set("Routing", profileRoutingRules)
}
}

View file

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

View file

@ -270,6 +270,9 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
obj["fp"], _ = fpValue.(string) obj["fp"], _ = fpValue.(string)
} }
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
obj["allowInsecure"], _ = insecure.(bool)
}
} }
} }
@ -293,7 +296,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
newSecurity, _ := ep["forceTls"].(string) newSecurity, _ := ep["forceTls"].(string)
newObj := map[string]any{} newObj := map[string]any{}
for key, value := range obj { for key, value := range obj {
if !(newSecurity == "none" && (key == "alpn" || key == "sni" || key == "fp")) { if !(newSecurity == "none" && (key == "alpn" || key == "sni" || key == "fp" || key == "allowInsecure")) {
newObj[key] = value newObj[key] = value
} }
} }
@ -428,6 +431,11 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string) params["fp"], _ = fpValue.(string)
} }
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
if insecure.(bool) {
params["allowInsecure"] = "1"
}
}
} }
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
@ -476,8 +484,8 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
externalProxies, _ := stream["externalProxy"].([]any) externalProxies, _ := stream["externalProxy"].([]any)
if len(externalProxies) > 0 { if len(externalProxies) > 0 {
links := make([]string, 0, len(externalProxies)) links := ""
for _, externalProxy := range externalProxies { for index, externalProxy := range externalProxies {
ep, _ := externalProxy.(map[string]any) ep, _ := externalProxy.(map[string]any)
newSecurity, _ := ep["forceTls"].(string) newSecurity, _ := ep["forceTls"].(string)
dest, _ := ep["dest"].(string) dest, _ := ep["dest"].(string)
@ -493,7 +501,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
q := url.Query() q := url.Query()
for k, v := range params { for k, v := range params {
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp")) { if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
q.Add(k, v) q.Add(k, v)
} }
} }
@ -503,9 +511,12 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string)) url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
links = append(links, url.String()) if index > 0 {
links += "\n"
} }
return strings.Join(links, "\n") links += url.String()
}
return links
} }
link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port) link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port)
@ -624,6 +635,11 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string) params["fp"], _ = fpValue.(string)
} }
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
if insecure.(bool) {
params["allowInsecure"] = "1"
}
}
} }
} }
@ -685,7 +701,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
q := url.Query() q := url.Query()
for k, v := range params { for k, v := range params {
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp")) { if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
q.Add(k, v) q.Add(k, v)
} }
} }
@ -824,6 +840,11 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string) params["fp"], _ = fpValue.(string)
} }
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
if insecure.(bool) {
params["allowInsecure"] = "1"
}
}
} }
} }
@ -852,7 +873,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
q := url.Query() q := url.Query()
for k, v := range params { for k, v := range params {
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp")) { if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
q.Add(k, v) q.Add(k, v)
} }
} }

350
update.sh
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,6 @@ const Protocols = {
MIXED: 'mixed', MIXED: 'mixed',
HTTP: 'http', HTTP: 'http',
WIREGUARD: 'wireguard', WIREGUARD: 'wireguard',
TUN: 'tun',
}; };
const SSMethods = { const SSMethods = {
@ -319,12 +318,14 @@ TcpStreamSettings.TcpResponse = class extends XrayCommonClass {
class KcpStreamSettings extends XrayCommonClass { class KcpStreamSettings extends XrayCommonClass {
constructor( constructor(
mtu = 1350, mtu = 1350,
tti = 20, tti = 50,
uplinkCapacity = 5, uplinkCapacity = 5,
downlinkCapacity = 20, downlinkCapacity = 20,
congestion = false, congestion = false,
readBufferSize = 1, readBufferSize = 2,
writeBufferSize = 1, writeBufferSize = 2,
type = 'none',
seed = RandomUtil.randomSeq(10),
) { ) {
super(); super();
this.mtu = mtu; this.mtu = mtu;
@ -334,6 +335,8 @@ class KcpStreamSettings extends XrayCommonClass {
this.congestion = congestion; this.congestion = congestion;
this.readBuffer = readBufferSize; this.readBuffer = readBufferSize;
this.writeBuffer = writeBufferSize; this.writeBuffer = writeBufferSize;
this.type = type;
this.seed = seed;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
@ -345,6 +348,8 @@ class KcpStreamSettings extends XrayCommonClass {
json.congestion, json.congestion,
json.readBufferSize, json.readBufferSize,
json.writeBufferSize, json.writeBufferSize,
ObjectUtil.isEmpty(json.header) ? 'none' : json.header.type,
json.seed,
); );
} }
@ -357,6 +362,10 @@ class KcpStreamSettings extends XrayCommonClass {
congestion: this.congestion, congestion: this.congestion,
readBufferSize: this.readBuffer, readBufferSize: this.readBuffer,
writeBufferSize: this.writeBuffer, writeBufferSize: this.writeBuffer,
header: {
type: this.type,
},
seed: this.seed,
}; };
} }
} }
@ -487,19 +496,6 @@ class xHTTPStreamSettings extends XrayCommonClass {
noSSEHeader = false, noSSEHeader = false,
xPaddingBytes = "100-1000", xPaddingBytes = "100-1000",
mode = MODE_OPTION.AUTO, mode = MODE_OPTION.AUTO,
xPaddingObfsMode = false,
xPaddingKey = '',
xPaddingHeader = '',
xPaddingPlacement = '',
xPaddingMethod = '',
uplinkHTTPMethod = '',
sessionPlacement = '',
sessionKey = '',
seqPlacement = '',
seqKey = '',
uplinkDataPlacement = '',
uplinkDataKey = '',
uplinkChunkSize = 0,
) { ) {
super(); super();
this.path = path; this.path = path;
@ -511,19 +507,6 @@ class xHTTPStreamSettings extends XrayCommonClass {
this.noSSEHeader = noSSEHeader; this.noSSEHeader = noSSEHeader;
this.xPaddingBytes = xPaddingBytes; this.xPaddingBytes = xPaddingBytes;
this.mode = mode; this.mode = mode;
this.xPaddingObfsMode = xPaddingObfsMode;
this.xPaddingKey = xPaddingKey;
this.xPaddingHeader = xPaddingHeader;
this.xPaddingPlacement = xPaddingPlacement;
this.xPaddingMethod = xPaddingMethod;
this.uplinkHTTPMethod = uplinkHTTPMethod;
this.sessionPlacement = sessionPlacement;
this.sessionKey = sessionKey;
this.seqPlacement = seqPlacement;
this.seqKey = seqKey;
this.uplinkDataPlacement = uplinkDataPlacement;
this.uplinkDataKey = uplinkDataKey;
this.uplinkChunkSize = uplinkChunkSize;
} }
addHeader(name, value) { addHeader(name, value) {
@ -545,19 +528,6 @@ class xHTTPStreamSettings extends XrayCommonClass {
json.noSSEHeader, json.noSSEHeader,
json.xPaddingBytes, json.xPaddingBytes,
json.mode, json.mode,
json.xPaddingObfsMode,
json.xPaddingKey,
json.xPaddingHeader,
json.xPaddingPlacement,
json.xPaddingMethod,
json.uplinkHTTPMethod,
json.sessionPlacement,
json.sessionKey,
json.seqPlacement,
json.seqKey,
json.uplinkDataPlacement,
json.uplinkDataKey,
json.uplinkChunkSize,
); );
} }
@ -572,19 +542,6 @@ class xHTTPStreamSettings extends XrayCommonClass {
noSSEHeader: this.noSSEHeader, noSSEHeader: this.noSSEHeader,
xPaddingBytes: this.xPaddingBytes, xPaddingBytes: this.xPaddingBytes,
mode: this.mode, mode: this.mode,
xPaddingObfsMode: this.xPaddingObfsMode,
xPaddingKey: this.xPaddingKey,
xPaddingHeader: this.xPaddingHeader,
xPaddingPlacement: this.xPaddingPlacement,
xPaddingMethod: this.xPaddingMethod,
uplinkHTTPMethod: this.uplinkHTTPMethod,
sessionPlacement: this.sessionPlacement,
sessionKey: this.sessionKey,
seqPlacement: this.seqPlacement,
seqKey: this.seqKey,
uplinkDataPlacement: this.uplinkDataPlacement,
uplinkDataKey: this.uplinkDataKey,
uplinkChunkSize: this.uplinkChunkSize,
}; };
} }
} }
@ -596,6 +553,7 @@ class TlsStreamSettings extends XrayCommonClass {
maxVersion = TLS_VERSION_OPTION.TLS13, maxVersion = TLS_VERSION_OPTION.TLS13,
cipherSuites = '', cipherSuites = '',
rejectUnknownSni = false, rejectUnknownSni = false,
verifyPeerCertInNames = ['dns.google', 'cloudflare-dns.com'],
disableSystemRoot = false, disableSystemRoot = false,
enableSessionResumption = false, enableSessionResumption = false,
certificates = [new TlsStreamSettings.Cert()], certificates = [new TlsStreamSettings.Cert()],
@ -610,6 +568,7 @@ class TlsStreamSettings extends XrayCommonClass {
this.maxVersion = maxVersion; this.maxVersion = maxVersion;
this.cipherSuites = cipherSuites; this.cipherSuites = cipherSuites;
this.rejectUnknownSni = rejectUnknownSni; this.rejectUnknownSni = rejectUnknownSni;
this.verifyPeerCertInNames = Array.isArray(verifyPeerCertInNames) ? verifyPeerCertInNames.join(",") : verifyPeerCertInNames;
this.disableSystemRoot = disableSystemRoot; this.disableSystemRoot = disableSystemRoot;
this.enableSessionResumption = enableSessionResumption; this.enableSessionResumption = enableSessionResumption;
this.certs = certificates; this.certs = certificates;
@ -635,7 +594,7 @@ class TlsStreamSettings extends XrayCommonClass {
} }
if (!ObjectUtil.isEmpty(json.settings)) { if (!ObjectUtil.isEmpty(json.settings)) {
settings = new TlsStreamSettings.Settings(json.settings.fingerprint, json.settings.echConfigList); settings = new TlsStreamSettings.Settings(json.settings.allowInsecure, json.settings.fingerprint, json.settings.echConfigList);
} }
return new TlsStreamSettings( return new TlsStreamSettings(
json.serverName, json.serverName,
@ -643,6 +602,7 @@ class TlsStreamSettings extends XrayCommonClass {
json.maxVersion, json.maxVersion,
json.cipherSuites, json.cipherSuites,
json.rejectUnknownSni, json.rejectUnknownSni,
json.verifyPeerCertInNames,
json.disableSystemRoot, json.disableSystemRoot,
json.enableSessionResumption, json.enableSessionResumption,
certs, certs,
@ -660,6 +620,7 @@ class TlsStreamSettings extends XrayCommonClass {
maxVersion: this.maxVersion, maxVersion: this.maxVersion,
cipherSuites: this.cipherSuites, cipherSuites: this.cipherSuites,
rejectUnknownSni: this.rejectUnknownSni, rejectUnknownSni: this.rejectUnknownSni,
verifyPeerCertInNames: this.verifyPeerCertInNames.split(","),
disableSystemRoot: this.disableSystemRoot, disableSystemRoot: this.disableSystemRoot,
enableSessionResumption: this.enableSessionResumption, enableSessionResumption: this.enableSessionResumption,
certificates: TlsStreamSettings.toJsonArray(this.certs), certificates: TlsStreamSettings.toJsonArray(this.certs),
@ -738,21 +699,25 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
TlsStreamSettings.Settings = class extends XrayCommonClass { TlsStreamSettings.Settings = class extends XrayCommonClass {
constructor( constructor(
allowInsecure = false,
fingerprint = UTLS_FINGERPRINT.UTLS_CHROME, fingerprint = UTLS_FINGERPRINT.UTLS_CHROME,
echConfigList = '', echConfigList = '',
) { ) {
super(); super();
this.allowInsecure = allowInsecure;
this.fingerprint = fingerprint; this.fingerprint = fingerprint;
this.echConfigList = echConfigList; this.echConfigList = echConfigList;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new TlsStreamSettings.Settings( return new TlsStreamSettings.Settings(
json.allowInsecure,
json.fingerprint, json.fingerprint,
json.echConfigList, json.echConfigList,
); );
} }
toJson() { toJson() {
return { return {
allowInsecure: this.allowInsecure,
fingerprint: this.fingerprint, fingerprint: this.fingerprint,
echConfigList: this.echConfigList echConfigList: this.echConfigList
}; };
@ -963,68 +928,6 @@ class SockoptStreamSettings extends XrayCommonClass {
} }
} }
class UdpMask extends XrayCommonClass {
constructor(type = 'salamander', settings = {}) {
super();
this.type = type;
this.settings = this._getDefaultSettings(type, settings);
}
_getDefaultSettings(type, settings = {}) {
switch (type) {
case 'salamander':
case 'mkcp-aes128gcm':
return { password: settings.password || '' };
case 'header-dns':
case 'xdns':
return { domain: settings.domain || '' };
case 'xicmp':
return { ip: settings.ip || '', id: settings.id ?? 0 };
case 'mkcp-original':
case 'header-dtls':
case 'header-srtp':
case 'header-utp':
case 'header-wechat':
case 'header-wireguard':
return {};
default:
return settings;
}
}
static fromJson(json = {}) {
return new UdpMask(
json.type || 'salamander',
json.settings || {}
);
}
toJson() {
return {
type: this.type,
settings: (this.settings && Object.keys(this.settings).length > 0) ? this.settings : undefined
};
}
}
class FinalMaskStreamSettings extends XrayCommonClass {
constructor(udp = []) {
super();
this.udp = Array.isArray(udp) ? udp.map(u => new UdpMask(u.type, u.settings)) : [new UdpMask(udp.type, udp.settings)];
}
static fromJson(json = {}) {
return new FinalMaskStreamSettings(json.udp || []);
}
toJson() {
return {
udp: this.udp.map(udp => udp.toJson())
};
}
}
class StreamSettings extends XrayCommonClass { class StreamSettings extends XrayCommonClass {
constructor(network = 'tcp', constructor(network = 'tcp',
security = 'none', security = 'none',
@ -1037,7 +940,6 @@ class StreamSettings extends XrayCommonClass {
grpcSettings = new GrpcStreamSettings(), grpcSettings = new GrpcStreamSettings(),
httpupgradeSettings = new HTTPUpgradeStreamSettings(), httpupgradeSettings = new HTTPUpgradeStreamSettings(),
xhttpSettings = new xHTTPStreamSettings(), xhttpSettings = new xHTTPStreamSettings(),
finalmask = new FinalMaskStreamSettings(),
sockopt = undefined, sockopt = undefined,
) { ) {
super(); super();
@ -1052,24 +954,9 @@ class StreamSettings extends XrayCommonClass {
this.grpc = grpcSettings; this.grpc = grpcSettings;
this.httpupgrade = httpupgradeSettings; this.httpupgrade = httpupgradeSettings;
this.xhttp = xhttpSettings; this.xhttp = xhttpSettings;
this.finalmask = finalmask;
this.sockopt = sockopt; this.sockopt = sockopt;
} }
addUdpMask(type = 'salamander') {
this.finalmask.udp.push(new UdpMask(type));
}
delUdpMask(index) {
if (this.finalmask.udp) {
this.finalmask.udp.splice(index, 1);
}
}
get hasFinalMask() {
return this.finalmask.udp && this.finalmask.udp.length > 0;
}
get isTls() { get isTls() {
return this.security === "tls"; return this.security === "tls";
} }
@ -1116,7 +1003,6 @@ class StreamSettings extends XrayCommonClass {
GrpcStreamSettings.fromJson(json.grpcSettings), GrpcStreamSettings.fromJson(json.grpcSettings),
HTTPUpgradeStreamSettings.fromJson(json.httpupgradeSettings), HTTPUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
xHTTPStreamSettings.fromJson(json.xhttpSettings), xHTTPStreamSettings.fromJson(json.xhttpSettings),
FinalMaskStreamSettings.fromJson(json.finalmask),
SockoptStreamSettings.fromJson(json.sockopt), SockoptStreamSettings.fromJson(json.sockopt),
); );
} }
@ -1135,7 +1021,6 @@ class StreamSettings extends XrayCommonClass {
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined, grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined, httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,
xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined, xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined,
finalmask: this.hasFinalMask ? this.finalmask.toJson() : undefined,
sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined, sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
}; };
} }
@ -1306,6 +1191,14 @@ class Inbound extends XrayCommonClass {
return null; return null;
} }
get kcpType() {
return this.stream.kcp.type;
}
get kcpSeed() {
return this.stream.kcp.seed;
}
get serviceName() { get serviceName() {
return this.stream.grpc.serviceName; return this.stream.grpc.serviceName;
} }
@ -1382,6 +1275,8 @@ class Inbound extends XrayCommonClass {
} }
} else if (network === 'kcp') { } else if (network === 'kcp') {
const kcp = this.stream.kcp; const kcp = this.stream.kcp;
obj.type = kcp.type;
obj.path = kcp.seed;
} else if (network === 'ws') { } else if (network === 'ws') {
const ws = this.stream.ws; const ws = this.stream.ws;
obj.path = ws.path; obj.path = ws.path;
@ -1413,6 +1308,9 @@ class Inbound extends XrayCommonClass {
if (this.stream.tls.alpn.length > 0) { if (this.stream.tls.alpn.length > 0) {
obj.alpn = this.stream.tls.alpn.join(','); obj.alpn = this.stream.tls.alpn.join(',');
} }
if (this.stream.tls.settings.allowInsecure) {
obj.allowInsecure = this.stream.tls.settings.allowInsecure;
}
} }
return 'vmess://' + Base64.encode(JSON.stringify(obj, null, 2)); return 'vmess://' + Base64.encode(JSON.stringify(obj, null, 2));
@ -1441,6 +1339,8 @@ class Inbound extends XrayCommonClass {
break; break;
case "kcp": case "kcp":
const kcp = this.stream.kcp; const kcp = this.stream.kcp;
params.set("headerType", kcp.type);
params.set("seed", kcp.seed);
break; break;
case "ws": case "ws":
const ws = this.stream.ws; const ws = this.stream.ws;
@ -1473,6 +1373,9 @@ class Inbound extends XrayCommonClass {
if (this.stream.isTls) { if (this.stream.isTls) {
params.set("fp", this.stream.tls.settings.fingerprint); params.set("fp", this.stream.tls.settings.fingerprint);
params.set("alpn", this.stream.tls.alpn); params.set("alpn", this.stream.tls.alpn);
if (this.stream.tls.settings.allowInsecure) {
params.set("allowInsecure", "1");
}
if (!ObjectUtil.isEmpty(this.stream.tls.sni)) { if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
params.set("sni", this.stream.tls.sni); params.set("sni", this.stream.tls.sni);
} }
@ -1541,6 +1444,8 @@ class Inbound extends XrayCommonClass {
break; break;
case "kcp": case "kcp":
const kcp = this.stream.kcp; const kcp = this.stream.kcp;
params.set("headerType", kcp.type);
params.set("seed", kcp.seed);
break; break;
case "ws": case "ws":
const ws = this.stream.ws; const ws = this.stream.ws;
@ -1573,6 +1478,9 @@ class Inbound extends XrayCommonClass {
if (this.stream.isTls) { if (this.stream.isTls) {
params.set("fp", this.stream.tls.settings.fingerprint); params.set("fp", this.stream.tls.settings.fingerprint);
params.set("alpn", this.stream.tls.alpn); params.set("alpn", this.stream.tls.alpn);
if (this.stream.tls.settings.allowInsecure) {
params.set("allowInsecure", "1");
}
if (this.stream.tls.settings.echConfigList?.length > 0) { if (this.stream.tls.settings.echConfigList?.length > 0) {
params.set("ech", this.stream.tls.settings.echConfigList); params.set("ech", this.stream.tls.settings.echConfigList);
} }
@ -1617,6 +1525,8 @@ class Inbound extends XrayCommonClass {
break; break;
case "kcp": case "kcp":
const kcp = this.stream.kcp; const kcp = this.stream.kcp;
params.set("headerType", kcp.type);
params.set("seed", kcp.seed);
break; break;
case "ws": case "ws":
const ws = this.stream.ws; const ws = this.stream.ws;
@ -1649,6 +1559,9 @@ class Inbound extends XrayCommonClass {
if (this.stream.isTls) { if (this.stream.isTls) {
params.set("fp", this.stream.tls.settings.fingerprint); params.set("fp", this.stream.tls.settings.fingerprint);
params.set("alpn", this.stream.tls.alpn); params.set("alpn", this.stream.tls.alpn);
if (this.stream.tls.settings.allowInsecure) {
params.set("allowInsecure", "1");
}
if (this.stream.tls.settings.echConfigList?.length > 0) { if (this.stream.tls.settings.echConfigList?.length > 0) {
params.set("ech", this.stream.tls.settings.echConfigList); params.set("ech", this.stream.tls.settings.echConfigList);
} }
@ -1826,7 +1739,6 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.MIXED: return new Inbound.MixedSettings(protocol); case Protocols.MIXED: return new Inbound.MixedSettings(protocol);
case Protocols.HTTP: return new Inbound.HttpSettings(protocol); case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol); case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
case Protocols.TUN: return new Inbound.TunSettings(protocol);
default: return null; default: return null;
} }
} }
@ -1841,7 +1753,6 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.MIXED: return Inbound.MixedSettings.fromJson(json); case Protocols.MIXED: return Inbound.MixedSettings.fromJson(json);
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json); case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json); case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
case Protocols.TUN: return Inbound.TunSettings.fromJson(json);
default: return null; default: return null;
} }
} }
@ -2033,9 +1944,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
json.selectedAuth = this.selectedAuth; json.selectedAuth = this.selectedAuth;
} }
// Only include testseed if at least one client has a flow set if (this.testseed && this.testseed.length >= 4) {
const hasFlow = this.vlesses && this.vlesses.some(vless => vless.flow && vless.flow !== '');
if (hasFlow && this.testseed && this.testseed.length >= 4) {
json.testseed = this.testseed; json.testseed = this.testseed;
} }
@ -2677,34 +2586,3 @@ Inbound.WireguardSettings.Peer = class extends XrayCommonClass {
}; };
} }
}; };
Inbound.TunSettings = class extends Inbound.Settings {
constructor(
protocol,
name = 'xray0',
mtu = 1500,
userLevel = 0
) {
super(protocol);
this.name = name;
this.mtu = mtu;
this.userLevel = userLevel;
}
static fromJson(json = {}) {
return new Inbound.TunSettings(
Protocols.TUN,
json.name ?? 'xray0',
json.mtu ?? json.MTU ?? 1500,
json.userLevel ?? 0
);
}
toJson() {
return {
name: this.name || 'xray0',
mtu: this.mtu || 1500,
userLevel: this.userLevel || 0,
};
}
};

View file

@ -8,8 +8,7 @@ const Protocols = {
Shadowsocks: "shadowsocks", Shadowsocks: "shadowsocks",
Socks: "socks", Socks: "socks",
HTTP: "http", HTTP: "http",
Wireguard: "wireguard", Wireguard: "wireguard"
Hysteria: "hysteria"
}; };
const SSMethods = { const SSMethods = {
@ -166,12 +165,14 @@ class TcpStreamSettings extends CommonClass {
class KcpStreamSettings extends CommonClass { class KcpStreamSettings extends CommonClass {
constructor( constructor(
mtu = 1350, mtu = 1350,
tti = 20, tti = 50,
uplinkCapacity = 5, uplinkCapacity = 5,
downlinkCapacity = 20, downlinkCapacity = 20,
congestion = false, congestion = false,
readBufferSize = 1, readBufferSize = 2,
writeBufferSize = 1, writeBufferSize = 2,
type = 'none',
seed = '',
) { ) {
super(); super();
this.mtu = mtu; this.mtu = mtu;
@ -181,6 +182,8 @@ class KcpStreamSettings extends CommonClass {
this.congestion = congestion; this.congestion = congestion;
this.readBuffer = readBufferSize; this.readBuffer = readBufferSize;
this.writeBuffer = writeBufferSize; this.writeBuffer = writeBufferSize;
this.type = type;
this.seed = seed;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
@ -192,6 +195,8 @@ class KcpStreamSettings extends CommonClass {
json.congestion, json.congestion,
json.readBufferSize, json.readBufferSize,
json.writeBufferSize, json.writeBufferSize,
ObjectUtil.isEmpty(json.header) ? 'none' : json.header.type,
json.seed,
); );
} }
@ -204,6 +209,10 @@ class KcpStreamSettings extends CommonClass {
congestion: this.congestion, congestion: this.congestion,
readBufferSize: this.readBuffer, readBufferSize: this.readBuffer,
writeBufferSize: this.writeBuffer, writeBufferSize: this.writeBuffer,
header: {
type: this.type,
},
seed: this.seed,
}; };
} }
} }
@ -345,17 +354,15 @@ class TlsStreamSettings extends CommonClass {
serverName = '', serverName = '',
alpn = [], alpn = [],
fingerprint = '', fingerprint = '',
allowInsecure = false,
echConfigList = '', echConfigList = '',
verifyPeerCertByName = '',
pinnedPeerCertSha256 = '',
) { ) {
super(); super();
this.serverName = serverName; this.serverName = serverName;
this.alpn = alpn; this.alpn = alpn;
this.fingerprint = fingerprint; this.fingerprint = fingerprint;
this.allowInsecure = allowInsecure;
this.echConfigList = echConfigList; this.echConfigList = echConfigList;
this.verifyPeerCertByName = verifyPeerCertByName;
this.pinnedPeerCertSha256 = pinnedPeerCertSha256;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
@ -363,9 +370,8 @@ class TlsStreamSettings extends CommonClass {
json.serverName, json.serverName,
json.alpn, json.alpn,
json.fingerprint, json.fingerprint,
json.allowInsecure,
json.echConfigList, json.echConfigList,
json.verifyPeerCertByName,
json.pinnedPeerCertSha256,
); );
} }
@ -374,9 +380,8 @@ class TlsStreamSettings extends CommonClass {
serverName: this.serverName, serverName: this.serverName,
alpn: this.alpn, alpn: this.alpn,
fingerprint: this.fingerprint, fingerprint: this.fingerprint,
echConfigList: this.echConfigList, allowInsecure: this.allowInsecure,
verifyPeerCertByName: this.verifyPeerCertByName, echConfigList: this.echConfigList
pinnedPeerCertSha256: this.pinnedPeerCertSha256
}; };
} }
} }
@ -419,102 +424,6 @@ class RealityStreamSettings extends CommonClass {
}; };
} }
}; };
class HysteriaStreamSettings extends CommonClass {
constructor(
version = 2,
auth = '',
congestion = '',
up = '0',
down = '0',
udphopPort = '',
udphopIntervalMin = 30,
udphopIntervalMax = 30,
initStreamReceiveWindow = 8388608,
maxStreamReceiveWindow = 8388608,
initConnectionReceiveWindow = 20971520,
maxConnectionReceiveWindow = 20971520,
maxIdleTimeout = 30,
keepAlivePeriod = 0,
disablePathMTUDiscovery = false
) {
super();
this.version = version;
this.auth = auth;
this.congestion = congestion;
this.up = up;
this.down = down;
this.udphopPort = udphopPort;
this.udphopIntervalMin = udphopIntervalMin;
this.udphopIntervalMax = udphopIntervalMax;
this.initStreamReceiveWindow = initStreamReceiveWindow;
this.maxStreamReceiveWindow = maxStreamReceiveWindow;
this.initConnectionReceiveWindow = initConnectionReceiveWindow;
this.maxConnectionReceiveWindow = maxConnectionReceiveWindow;
this.maxIdleTimeout = maxIdleTimeout;
this.keepAlivePeriod = keepAlivePeriod;
this.disablePathMTUDiscovery = disablePathMTUDiscovery;
}
static fromJson(json = {}) {
let udphopPort = '';
let udphopIntervalMin = 30;
let udphopIntervalMax = 30;
if (json.udphop) {
udphopPort = json.udphop.port || '';
// Backward compatibility: if old 'interval' exists, use it for both min/max
if (json.udphop.interval !== undefined) {
udphopIntervalMin = json.udphop.interval;
udphopIntervalMax = json.udphop.interval;
} else {
udphopIntervalMin = json.udphop.intervalMin || 30;
udphopIntervalMax = json.udphop.intervalMax || 30;
}
}
return new HysteriaStreamSettings(
json.version,
json.auth,
json.congestion,
json.up,
json.down,
udphopPort,
udphopIntervalMin,
udphopIntervalMax,
json.initStreamReceiveWindow,
json.maxStreamReceiveWindow,
json.initConnectionReceiveWindow,
json.maxConnectionReceiveWindow,
json.maxIdleTimeout,
json.keepAlivePeriod,
json.disablePathMTUDiscovery
);
}
toJson() {
const result = {
version: this.version,
auth: this.auth,
congestion: this.congestion,
up: this.up,
down: this.down,
initStreamReceiveWindow: this.initStreamReceiveWindow,
maxStreamReceiveWindow: this.maxStreamReceiveWindow,
initConnectionReceiveWindow: this.initConnectionReceiveWindow,
maxConnectionReceiveWindow: this.maxConnectionReceiveWindow,
maxIdleTimeout: this.maxIdleTimeout,
keepAlivePeriod: this.keepAlivePeriod,
disablePathMTUDiscovery: this.disablePathMTUDiscovery
};
if (this.udphopPort) {
result.udphop = {
port: this.udphopPort,
intervalMin: this.udphopIntervalMin,
intervalMax: this.udphopIntervalMax
};
}
return result;
}
};
class SockoptStreamSettings extends CommonClass { class SockoptStreamSettings extends CommonClass {
constructor( constructor(
dialerProxy = "", dialerProxy = "",
@ -564,66 +473,6 @@ class SockoptStreamSettings extends CommonClass {
} }
} }
class UdpMask extends CommonClass {
constructor(type = 'salamander', settings = {}) {
super();
this.type = type;
this.settings = this._getDefaultSettings(type, settings);
}
_getDefaultSettings(type, settings = {}) {
switch (type) {
case 'salamander':
case 'mkcp-aes128gcm':
return { password: settings.password || '' };
case 'header-dns':
case 'xdns':
return { domain: settings.domain || '' };
case 'mkcp-original':
case 'header-dtls':
case 'header-srtp':
case 'header-utp':
case 'header-wechat':
case 'header-wireguard':
return {}; // No settings needed
default:
return settings;
}
}
static fromJson(json = {}) {
return new UdpMask(
json.type || 'salamander',
json.settings || {}
);
}
toJson() {
return {
type: this.type,
settings: (this.settings && Object.keys(this.settings).length > 0) ? this.settings : undefined
};
}
}
class FinalMaskStreamSettings extends CommonClass {
constructor(udp = []) {
super();
this.udp = Array.isArray(udp) ? udp.map(u => new UdpMask(u.type, u.settings)) : [new UdpMask(udp.type, udp.settings)];
}
static fromJson(json = {}) {
return new FinalMaskStreamSettings(json.udp || []);
}
toJson() {
return {
udp: this.udp.map(udp => udp.toJson())
};
}
}
class StreamSettings extends CommonClass { class StreamSettings extends CommonClass {
constructor( constructor(
network = 'tcp', network = 'tcp',
@ -636,8 +485,6 @@ class StreamSettings extends CommonClass {
grpcSettings = new GrpcStreamSettings(), grpcSettings = new GrpcStreamSettings(),
httpupgradeSettings = new HttpUpgradeStreamSettings(), httpupgradeSettings = new HttpUpgradeStreamSettings(),
xhttpSettings = new xHTTPStreamSettings(), xhttpSettings = new xHTTPStreamSettings(),
hysteriaSettings = new HysteriaStreamSettings(),
finalmask = new FinalMaskStreamSettings(),
sockopt = undefined, sockopt = undefined,
) { ) {
super(); super();
@ -651,25 +498,9 @@ class StreamSettings extends CommonClass {
this.grpc = grpcSettings; this.grpc = grpcSettings;
this.httpupgrade = httpupgradeSettings; this.httpupgrade = httpupgradeSettings;
this.xhttp = xhttpSettings; this.xhttp = xhttpSettings;
this.hysteria = hysteriaSettings;
this.finalmask = finalmask;
this.sockopt = sockopt; this.sockopt = sockopt;
} }
addUdpMask(type = 'salamander') {
this.finalmask.udp.push(new UdpMask(type));
}
delUdpMask(index) {
if (this.finalmask.udp) {
this.finalmask.udp.splice(index, 1);
}
}
get hasFinalMask() {
return this.finalmask.udp && this.finalmask.udp.length > 0;
}
get isTls() { get isTls() {
return this.security === 'tls'; return this.security === 'tls';
} }
@ -698,8 +529,6 @@ class StreamSettings extends CommonClass {
GrpcStreamSettings.fromJson(json.grpcSettings), GrpcStreamSettings.fromJson(json.grpcSettings),
HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings), HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
xHTTPStreamSettings.fromJson(json.xhttpSettings), xHTTPStreamSettings.fromJson(json.xhttpSettings),
HysteriaStreamSettings.fromJson(json.hysteriaSettings),
FinalMaskStreamSettings.fromJson(json.finalmask),
SockoptStreamSettings.fromJson(json.sockopt), SockoptStreamSettings.fromJson(json.sockopt),
); );
} }
@ -717,8 +546,6 @@ class StreamSettings extends CommonClass {
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined, grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined, httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,
xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined, xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined,
hysteriaSettings: network === 'hysteria' ? this.hysteria.toJson() : undefined,
finalmask: this.hasFinalMask ? this.finalmask.toJson() : undefined,
sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined, sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
}; };
} }
@ -782,8 +609,7 @@ class Outbound extends CommonClass {
} }
canEnableTls() { canEnableTls() {
if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks, Protocols.Hysteria].includes(this.protocol)) return false; if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol)) return false;
if (this.protocol === Protocols.Hysteria) return this.stream.network === 'hysteria';
return ["tcp", "ws", "http", "grpc", "httpupgrade", "xhttp"].includes(this.stream.network); return ["tcp", "ws", "http", "grpc", "httpupgrade", "xhttp"].includes(this.stream.network);
} }
@ -808,7 +634,7 @@ class Outbound extends CommonClass {
} }
canEnableStream() { canEnableStream() {
return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks, Protocols.Hysteria].includes(this.protocol); return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol);
} }
canEnableMux() { canEnableMux() {
@ -847,8 +673,7 @@ class Outbound extends CommonClass {
Protocols.Trojan, Protocols.Trojan,
Protocols.Shadowsocks, Protocols.Shadowsocks,
Protocols.Socks, Protocols.Socks,
Protocols.HTTP, Protocols.HTTP
Protocols.Hysteria
].includes(this.protocol); ].includes(this.protocol);
} }
@ -897,9 +722,6 @@ class Outbound extends CommonClass {
case Protocols.Trojan: case Protocols.Trojan:
case 'ss': case 'ss':
return this.fromParamLink(link); return this.fromParamLink(link);
case 'hysteria2':
case Protocols.Hysteria:
return this.fromHysteriaLink(link);
default: default:
return null; return null;
} }
@ -932,7 +754,8 @@ class Outbound extends CommonClass {
stream.tls = new TlsStreamSettings( stream.tls = new TlsStreamSettings(
json.sni, json.sni,
json.alpn ? json.alpn.split(',') : [], json.alpn ? json.alpn.split(',') : [],
json.fp); json.fp,
json.allowInsecure);
} }
const port = json.port * 1; const port = json.port * 1;
@ -973,9 +796,10 @@ class Outbound extends CommonClass {
if (security == 'tls') { if (security == 'tls') {
let fp = url.searchParams.get('fp') ?? 'none'; let fp = url.searchParams.get('fp') ?? 'none';
let alpn = url.searchParams.get('alpn'); let alpn = url.searchParams.get('alpn');
let allowInsecure = url.searchParams.get('allowInsecure');
let sni = url.searchParams.get('sni') ?? ''; let sni = url.searchParams.get('sni') ?? '';
let ech = url.searchParams.get('ech') ?? ''; let ech = url.searchParams.get('ech') ?? '';
stream.tls = new TlsStreamSettings(sni, alpn ? alpn.split(',') : [], fp, ech); stream.tls = new TlsStreamSettings(sni, alpn ? alpn.split(',') : [], fp, allowInsecure == 1, ech);
} }
if (security == 'reality') { if (security == 'reality') {
@ -1018,70 +842,6 @@ class Outbound extends CommonClass {
remark = remark.length > 0 ? remark.substring(1) : 'out-' + protocol + '-' + port; remark = remark.length > 0 ? remark.substring(1) : 'out-' + protocol + '-' + port;
return new Outbound(remark, protocol, settings, stream); return new Outbound(remark, protocol, settings, stream);
} }
static fromHysteriaLink(link) {
// Parse hysteria2://password@address:port[?param1=value1&param2=value2...][#remarks]
const regex = /^hysteria2?:\/\/([^@]+)@([^:?#]+):(\d+)([^#]*)(#.*)?$/;
const match = link.match(regex);
if (!match) return null;
let [, password, address, port, params, hash] = match;
port = parseInt(port);
// Parse URL parameters if present
let urlParams = new URLSearchParams(params);
// Create stream settings with hysteria network
let stream = new StreamSettings('hysteria', 'none');
// Set hysteria stream settings
stream.hysteria.auth = password;
stream.hysteria.congestion = urlParams.get('congestion') ?? '';
stream.hysteria.up = urlParams.get('up') ?? '0';
stream.hysteria.down = urlParams.get('down') ?? '0';
stream.hysteria.udphopPort = urlParams.get('udphopPort') ?? '';
// Support both old single interval and new min/max range
if (urlParams.has('udphopInterval')) {
const interval = parseInt(urlParams.get('udphopInterval'));
stream.hysteria.udphopIntervalMin = interval;
stream.hysteria.udphopIntervalMax = interval;
} else {
stream.hysteria.udphopIntervalMin = parseInt(urlParams.get('udphopIntervalMin') ?? '30');
stream.hysteria.udphopIntervalMax = parseInt(urlParams.get('udphopIntervalMax') ?? '30');
}
// Optional QUIC parameters
if (urlParams.has('initStreamReceiveWindow')) {
stream.hysteria.initStreamReceiveWindow = parseInt(urlParams.get('initStreamReceiveWindow'));
}
if (urlParams.has('maxStreamReceiveWindow')) {
stream.hysteria.maxStreamReceiveWindow = parseInt(urlParams.get('maxStreamReceiveWindow'));
}
if (urlParams.has('initConnectionReceiveWindow')) {
stream.hysteria.initConnectionReceiveWindow = parseInt(urlParams.get('initConnectionReceiveWindow'));
}
if (urlParams.has('maxConnectionReceiveWindow')) {
stream.hysteria.maxConnectionReceiveWindow = parseInt(urlParams.get('maxConnectionReceiveWindow'));
}
if (urlParams.has('maxIdleTimeout')) {
stream.hysteria.maxIdleTimeout = parseInt(urlParams.get('maxIdleTimeout'));
}
if (urlParams.has('keepAlivePeriod')) {
stream.hysteria.keepAlivePeriod = parseInt(urlParams.get('keepAlivePeriod'));
}
if (urlParams.has('disablePathMTUDiscovery')) {
stream.hysteria.disablePathMTUDiscovery = urlParams.get('disablePathMTUDiscovery') === 'true';
}
// Create settings
let settings = new Outbound.HysteriaSettings(address, port, 2);
// Extract remark from hash
let remark = hash ? decodeURIComponent(hash.substring(1)) : `out-hysteria-${port}`;
return new Outbound(remark, Protocols.Hysteria, settings, stream);
}
} }
Outbound.Settings = class extends CommonClass { Outbound.Settings = class extends CommonClass {
@ -1102,7 +862,6 @@ Outbound.Settings = class extends CommonClass {
case Protocols.Socks: return new Outbound.SocksSettings(); case Protocols.Socks: return new Outbound.SocksSettings();
case Protocols.HTTP: return new Outbound.HttpSettings(); case Protocols.HTTP: return new Outbound.HttpSettings();
case Protocols.Wireguard: return new Outbound.WireguardSettings(); case Protocols.Wireguard: return new Outbound.WireguardSettings();
case Protocols.Hysteria: return new Outbound.HysteriaSettings();
default: return null; default: return null;
} }
} }
@ -1119,7 +878,6 @@ Outbound.Settings = class extends CommonClass {
case Protocols.Socks: return Outbound.SocksSettings.fromJson(json); case Protocols.Socks: return Outbound.SocksSettings.fromJson(json);
case Protocols.HTTP: return Outbound.HttpSettings.fromJson(json); case Protocols.HTTP: return Outbound.HttpSettings.fromJson(json);
case Protocols.Wireguard: return Outbound.WireguardSettings.fromJson(json); case Protocols.Wireguard: return Outbound.WireguardSettings.fromJson(json);
case Protocols.Hysteria: return Outbound.HysteriaSettings.fromJson(json);
default: return null; default: return null;
} }
} }
@ -1338,15 +1096,12 @@ Outbound.VLESSSettings = class extends CommonClass {
flow: this.flow, flow: this.flow,
encryption: this.encryption, encryption: this.encryption,
}; };
// Only include Vision settings when flow is set
if (this.flow && this.flow !== '') {
if (this.testpre > 0) { if (this.testpre > 0) {
result.testpre = this.testpre; result.testpre = this.testpre;
} }
if (this.testseed && this.testseed.length >= 4) { if (this.testseed && this.testseed.length >= 4) {
result.testseed = this.testseed; result.testseed = this.testseed;
} }
}
return result; return result;
} }
}; };
@ -1570,29 +1325,3 @@ Outbound.WireguardSettings.Peer = class extends CommonClass {
}; };
} }
}; };
Outbound.HysteriaSettings = class extends CommonClass {
constructor(address = '', port = 443, version = 2) {
super();
this.address = address;
this.port = port;
this.version = version;
}
static fromJson(json = {}) {
if (Object.keys(json).length === 0) return new Outbound.HysteriaSettings();
return new Outbound.HysteriaSettings(
json.address,
json.port,
json.version
);
}
toJson() {
return {
address: this.address,
port: this.port,
version: this.version
};
}
};

View file

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

View file

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

View file

@ -908,10 +908,7 @@ class IntlUtil {
const language = LanguageManager.getLanguage() const language = LanguageManager.getLanguage()
const now = new Date() const now = new Date()
// Handle delayed start (negative expiryTime values) const diff = Math.round((date - now) / (1000 * 60 * 60 * 24))
const diff = date < 0
? Math.round(date / (1000 * 60 * 60 * 24))
: Math.round((date - now) / (1000 * 60 * 60 * 24))
const formatter = new Intl.RelativeTimeFormat(language, { numeric: 'auto' }) const formatter = new Intl.RelativeTimeFormat(language, { numeric: 'auto' })
return formatter.format(diff, 'day'); return formatter.format(diff, 'day');

View file

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

View file

@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"strconv" "strconv"
"time"
"github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/database/model"
"github.com/mhsanaei/3x-ui/v2/web/service" "github.com/mhsanaei/3x-ui/v2/web/service"
@ -194,37 +193,6 @@ func (a *InboundController) getClientIps(c *gin.Context) {
return return
} }
// Prefer returning a normalized string list for consistent UI rendering
type ipWithTimestamp struct {
IP string `json:"ip"`
Timestamp int64 `json:"timestamp"`
}
var ipsWithTime []ipWithTimestamp
if err := json.Unmarshal([]byte(ips), &ipsWithTime); err == nil && len(ipsWithTime) > 0 {
formatted := make([]string, 0, len(ipsWithTime))
for _, item := range ipsWithTime {
if item.IP == "" {
continue
}
if item.Timestamp > 0 {
ts := time.Unix(item.Timestamp, 0).Local().Format("2006-01-02 15:04:05")
formatted = append(formatted, fmt.Sprintf("%s (%s)", item.IP, ts))
continue
}
formatted = append(formatted, item.IP)
}
jsonObj(c, formatted, nil)
return
}
var oldIps []string
if err := json.Unmarshal([]byte(ips), &oldIps); err == nil && len(oldIps) > 0 {
jsonObj(c, oldIps, nil)
return
}
// If parsing fails, return as string
jsonObj(c, ips, nil) jsonObj(c, ips, nil)
} }

View file

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

View file

@ -1,9 +1,6 @@
package controller package controller
import ( import (
"encoding/json"
"github.com/mhsanaei/3x-ui/v2/util/common"
"github.com/mhsanaei/3x-ui/v2/web/service" "github.com/mhsanaei/3x-ui/v2/web/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -37,10 +34,9 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
g.POST("/warp/:action", a.warp) g.POST("/warp/:action", a.warp)
g.POST("/update", a.updateSetting) g.POST("/update", a.updateSetting)
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic) g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
g.POST("/testOutbound", a.testOutbound)
} }
// getXraySetting retrieves the Xray configuration template, inbound tags, and outbound test URL. // getXraySetting retrieves the Xray configuration template and inbound tags.
func (a *XraySettingController) getXraySetting(c *gin.Context) { func (a *XraySettingController) getXraySetting(c *gin.Context) {
xraySetting, err := a.SettingService.GetXrayConfigTemplate() xraySetting, err := a.SettingService.GetXrayConfigTemplate()
if err != nil { if err != nil {
@ -52,36 +48,15 @@ func (a *XraySettingController) getXraySetting(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return return
} }
outboundTestUrl, _ := a.SettingService.GetXrayOutboundTestUrl() xrayResponse := "{ \"xraySetting\": " + xraySetting + ", \"inboundTags\": " + inboundTags + " }"
if outboundTestUrl == "" { jsonObj(c, xrayResponse, nil)
outboundTestUrl = "https://www.google.com/generate_204"
}
xrayResponse := map[string]interface{}{
"xraySetting": json.RawMessage(xraySetting),
"inboundTags": json.RawMessage(inboundTags),
"outboundTestUrl": outboundTestUrl,
}
result, err := json.Marshal(xrayResponse)
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
jsonObj(c, string(result), nil)
} }
// updateSetting updates the Xray configuration settings. // updateSetting updates the Xray configuration settings.
func (a *XraySettingController) updateSetting(c *gin.Context) { func (a *XraySettingController) updateSetting(c *gin.Context) {
xraySetting := c.PostForm("xraySetting") xraySetting := c.PostForm("xraySetting")
if err := a.XraySettingService.SaveXraySetting(xraySetting); err != nil { err := a.XraySettingService.SaveXraySetting(xraySetting)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
return
}
outboundTestUrl := c.PostForm("outboundTestUrl")
if outboundTestUrl == "" {
outboundTestUrl = "https://www.google.com/generate_204"
}
_ = a.SettingService.SetXrayOutboundTestUrl(outboundTestUrl)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), nil)
} }
// getDefaultXrayConfig retrieves the default Xray configuration. // getDefaultXrayConfig retrieves the default Xray configuration.
@ -143,26 +118,3 @@ func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) {
} }
jsonObj(c, "", nil) jsonObj(c, "", nil)
} }
// testOutbound tests an outbound configuration and returns the delay/response time.
// Optional form "allOutbounds": JSON array of all outbounds; used to resolve sockopt.dialerProxy dependencies.
func (a *XraySettingController) testOutbound(c *gin.Context) {
outboundJSON := c.PostForm("outbound")
allOutboundsJSON := c.PostForm("allOutbounds")
if outboundJSON == "" {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("outbound parameter is required"))
return
}
// Load the test URL from server settings to prevent SSRF via user-controlled URLs
testURL, _ := a.SettingService.GetXrayOutboundTestUrl()
result, err := a.OutboundService.TestOutbound(outboundJSON, testURL, allOutboundsJSON)
if err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
jsonObj(c, result, nil)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
{{define "form/vless"}} {{define "form/vless"}}
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit"> <a-collapse activeKey="0"
v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
{{template "form/client"}} {{template "form/client"}}
</a-collapse-panel> </a-collapse-panel>
@ -21,7 +22,8 @@
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
<template v-if=" !inbound.stream.isTLS || !inbound.stream.isReality"> <template v-if=" !inbound.stream.isTLS || !inbound.stream.isReality">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-form-item label="Authentication"> <a-form-item label="Authentication">
<a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc" <a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
@ -45,19 +47,24 @@
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-form> </a-form>
<a-divider :style="{ margin: '5px 0' }"></a-divider> <a-divider v-if="inbound.settings.selectedAuth"
:style="{ margin: '5px 0' }"></a-divider>
</template> </template>
<template v-if="inbound.isTcp && !inbound.settings.selectedAuth"> <template v-if="inbound.isTcp && !inbound.settings.selectedAuth">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks"> <a-form-item label="Fallbacks">
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button> <a-button icon="plus" type="primary" size="small"
@click="inbound.settings.addFallback()"></a-button>
</a-form-item> </a-form-item>
</a-form> </a-form>
<!-- vless fallbacks --> <!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }" <a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"> :wrapper-col="{ md: {span:14} }">
<a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete" <a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon
type="delete"
@click="() => inbound.settings.delFallback(index)" @click="() => inbound.settings.delFallback(index)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon> :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider> </a-divider>
@ -74,37 +81,43 @@
<a-input v-model="fallback.dest"></a-input> <a-input v-model="fallback.dest"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='xVer'> <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.number="fallback.xver" :min="0"
:max="2"></a-input-number>
</a-form-item> </a-form-item>
</a-form> </a-form>
<a-divider :style="{ margin: '5px 0' }"></a-divider> <a-divider :style="{ margin: '5px 0' }"></a-divider>
</template> </template>
<template v-if="inbound.canEnableVisionSeed()"> <template v-if="inbound.canEnableVisionSeed()">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-form-item label="Vision Seed"> <a-form-item label="Vision Seed">
<a-row :gutter="8"> <a-row :gutter="8">
<a-col :span="6"> <a-col :span="6">
<a-input-number <a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900" :value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900"
@change="(val) => updateTestseed(0, val)" :min="0" :max="9999" :style="{ width: '100%' }" @change="(val) => updateTestseed(0, val)" :min="0" :max="9999"
:style="{ width: '100%' }"
placeholder="900" addon-before="[0]"></a-input-number> placeholder="900" addon-before="[0]"></a-input-number>
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6">
<a-input-number <a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500" :value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500"
@change="(val) => updateTestseed(1, val)" :min="0" :max="9999" :style="{ width: '100%' }" @change="(val) => updateTestseed(1, val)" :min="0" :max="9999"
:style="{ width: '100%' }"
placeholder="500" addon-before="[1]"></a-input-number> placeholder="500" addon-before="[1]"></a-input-number>
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6">
<a-input-number <a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900" :value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900"
@change="(val) => updateTestseed(2, val)" :min="0" :max="9999" :style="{ width: '100%' }" @change="(val) => updateTestseed(2, val)" :min="0" :max="9999"
:style="{ width: '100%' }"
placeholder="900" addon-before="[2]"></a-input-number> placeholder="900" addon-before="[2]"></a-input-number>
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6">
<a-input-number <a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256" :value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256"
@change="(val) => updateTestseed(3, val)" :min="0" :max="9999" :style="{ width: '100%' }" @change="(val) => updateTestseed(3, val)" :min="0" :max="9999"
:style="{ width: '100%' }"
placeholder="256" addon-before="[3]"></a-input-number> placeholder="256" addon-before="[3]"></a-input-number>
</a-col> </a-col>
</a-row> </a-row>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,5 @@
{{define "modals/inboundInfoModal"}} {{define "modals/inboundInfoModal"}}
<a-modal id="inbound-info-modal" v-model="infoModal.visible" <a-modal id="inbound-info-modal" v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}' :closable="true" :mask-closable="true" :footer="null" width="600px" :class="themeSwitcher.currentTheme">
title='{{ i18n "pages.inbounds.details"}}' :closable="true"
:mask-closable="true" :footer="null" width="600px"
:class="themeSwitcher.currentTheme">
<a-row> <a-row>
<a-col :xs="24" :md="12"> <a-col :xs="24" :md="12">
<table> <table>
@ -29,8 +26,7 @@
</table> </table>
</a-col> </a-col>
<a-col :xs="24" :md="12"> <a-col :xs="24" :md="12">
<template <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<table> <table>
<tr> <tr>
<td>{{ i18n "transmission" }}</td> <td>{{ i18n "transmission" }}</td>
@ -38,8 +34,7 @@
<a-tag color="green">[[ inbound.network ]]</a-tag> <a-tag color="green">[[ inbound.network ]]</a-tag>
</td> </td>
</tr> </tr>
<template <template v-if="inbound.isTcp || inbound.isWs || inbound.isHttpupgrade || inbound.isXHTTP">
v-if="inbound.isTcp || inbound.isWs || inbound.isHttpupgrade || inbound.isXHTTP">
<tr> <tr>
<td>{{ i18n "host" }}</td> <td>{{ i18n "host" }}</td>
<td v-if="inbound.host"> <td v-if="inbound.host">
@ -71,13 +66,26 @@
</td> </td>
</tr> </tr>
</template> </template>
<template v-if="inbound.isKcp">
<tr>
<td>kcp {{ i18n "encryption" }}</td>
<td>
<a-tag>[[ inbound.kcpType ]]</a-tag>
</td>
</tr>
<tr>
<td>kcp {{ i18n "password" }}</td>
<td>
<a-tag>[[ inbound.kcpSeed ]]</a-tag>
</td>
</tr>
</template>
<template v-if="inbound.isGrpc"> <template v-if="inbound.isGrpc">
<tr> <tr>
<td>grpc serviceName</td> <td>grpc serviceName</td>
<td> <td>
<a-tooltip :title="[[ inbound.serviceName ]]"> <a-tooltip :title="[[ inbound.serviceName ]]">
<a-tag class="info-large-tag">[[ inbound.serviceName <a-tag class="info-large-tag">[[ inbound.serviceName ]]</a-tag>
]]</a-tag>
</a-tooltip> </a-tooltip>
<tr> <tr>
<td>grpc multiMode</td> <td>grpc multiMode</td>
@ -91,34 +99,25 @@
</a-col> </a-col>
<template v-if="dbInbound.hasLink()"> <template v-if="dbInbound.hasLink()">
{{ i18n "security" }} {{ i18n "security" }}
<a-tag :color="inbound.stream.security == 'none' ? 'red' : 'green'">[[ <a-tag :color="inbound.stream.security == 'none' ? 'red' : 'green'">[[ inbound.stream.security ]]</a-tag>
inbound.stream.security ]]</a-tag>
<br /> <br />
<td>Authentication</td> <td>Authentication</td>
<a-tag v-if="inbound.settings.selectedAuth" color="green">[[ <a-tag v-if="inbound.settings.selectedAuth" color="green">[[ inbound.settings.selectedAuth ? inbound.settings.selectedAuth : '' ]]</a-tag>
inbound.settings.selectedAuth ? inbound.settings.selectedAuth : ''
]]</a-tag>
<a-tag v-else color="red">{{ i18n "none" }}</a-tag> <a-tag v-else color="red">{{ i18n "none" }}</a-tag>
<br /> <br />
{{ i18n "encryption" }} {{ i18n "encryption" }}
<a-tag class="info-large-tag" <a-tag class="info-large-tag" :color="inbound.settings.encryption ? 'green' : 'red'">[[ inbound.settings.encryption ? inbound.settings.encryption : '' ]]</a-tag>
:color="inbound.settings.encryption ? 'green' : 'red'">[[
inbound.settings.encryption ? inbound.settings.encryption : ''
]]</a-tag>
<a-tooltip title='{{ i18n "copy" }}'> <a-tooltip title='{{ i18n "copy" }}'>
<a-button size="small" icon="snippets" <a-button size="small" icon="snippets" @click="copy(inbound.settings.encryption)"></a-button>
@click="copy(inbound.settings.encryption)"></a-button>
</a-tooltip> </a-tooltip>
<br /> <br />
<template v-if="inbound.stream.security != 'none'"> <template v-if="inbound.stream.security != 'none'">
{{ i18n "domainName" }} {{ i18n "domainName" }}
<a-tag v-if="inbound.serverName" color="green">[[ inbound.serverName <a-tag v-if="inbound.serverName" color="green">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
? inbound.serverName : '' ]]</a-tag>
<a-tag v-else color="orange">{{ i18n "none" }}</a-tag> <a-tag v-else color="orange">{{ i18n "none" }}</a-tag>
</template> </template>
</template> </template>
<table v-if="dbInbound.isSS" <table v-if="dbInbound.isSS" :style="{ marginBottom: '10px', width: '100%' }">
:style="{ marginBottom: '10px', width: '100%' }">
<tr> <tr>
<td>{{ i18n "encryption" }}</td> <td>{{ i18n "encryption" }}</td>
<td> <td>
@ -129,8 +128,7 @@
<td>{{ i18n "password" }}</td> <td>{{ i18n "password" }}</td>
<td> <td>
<a-tooltip :title="[[ inbound.settings.password ]]"> <a-tooltip :title="[[ inbound.settings.password ]]">
<a-tag class="info-large-tag">[[ inbound.settings.password <a-tag class="info-large-tag">[[ inbound.settings.password ]]</a-tag>
]]</a-tag>
</a-tooltip> </a-tooltip>
</td> </td>
</tr> </tr>
@ -147,8 +145,7 @@
<tr> <tr>
<td>{{ i18n "pages.inbounds.email" }}</td> <td>{{ i18n "pages.inbounds.email" }}</td>
<td v-if="infoModal.clientSettings.email"> <td v-if="infoModal.clientSettings.email">
<a-tag color="green">[[ infoModal.clientSettings.email <a-tag color="green">[[ infoModal.clientSettings.email ]]</a-tag>
]]</a-tag>
</td> </td>
<td v-else> <td v-else>
<a-tag color="red">{{ i18n "none" }}</a-tag> <a-tag color="red">{{ i18n "none" }}</a-tag>
@ -179,40 +176,30 @@
<td>{{ i18n "password" }}</td> <td>{{ i18n "password" }}</td>
<td> <td>
<a-tooltip :title="[[ infoModal.clientSettings.password ]]"> <a-tooltip :title="[[ infoModal.clientSettings.password ]]">
<a-tag class="info-large-tag">[[ <a-tag class="info-large-tag">[[ infoModal.clientSettings.password ]]</a-tag>
infoModal.clientSettings.password ]]</a-tag>
</a-tooltip> </a-tooltip>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>{{ i18n "status" }}</td> <td>{{ i18n "status" }}</td>
<td> <td>
<a-tag v-if="isDepleted" color="red">{{ i18n "depleted" <a-tag v-if="isDepleted" color="red">{{ i18n "depleted" }}</a-tag>
}}</a-tag> <a-tag v-else-if="isEnable" color="green">{{ i18n "enabled" }}</a-tag>
<a-tag v-else-if="isEnable" color="green">{{ i18n "enabled"
}}</a-tag>
<a-tag v-else>{{ i18n "disabled" }}</a-tag> <a-tag v-else>{{ i18n "disabled" }}</a-tag>
</td> </td>
</tr> </tr>
<tr v-if="infoModal.clientStats"> <tr v-if="infoModal.clientStats">
<td>{{ i18n "usage" }}</td> <td>{{ i18n "usage" }}</td>
<td> <td>
<a-tag color="green">[[ <a-tag color="green">[[ SizeFormatter.sizeFormat(infoModal.clientStats.up + infoModal.clientStats.down) ]]</a-tag>
SizeFormatter.sizeFormat(infoModal.clientStats.up + <a-tag>↑ [[ SizeFormatter.sizeFormat(infoModal.clientStats.up) ]] / [[ SizeFormatter.sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
infoModal.clientStats.down) ]]</a-tag>
<a-tag>↑ [[ SizeFormatter.sizeFormat(infoModal.clientStats.up)
]] / [[ SizeFormatter.sizeFormat(infoModal.clientStats.down)
]] ↓</a-tag>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>{{ i18n "pages.inbounds.createdAt" }}</td> <td>{{ i18n "pages.inbounds.createdAt" }}</td>
<td> <td>
<template <template v-if="infoModal.clientSettings && infoModal.clientSettings.created_at">
v-if="infoModal.clientSettings && infoModal.clientSettings.created_at"> <a-tag>[[ IntlUtil.formatDate(infoModal.clientSettings.created_at) ]]</a-tag>
<a-tag>[[
IntlUtil.formatDate(infoModal.clientSettings.created_at)
]]</a-tag>
</template> </template>
<template v-else> <template v-else>
<a-tag>-</a-tag> <a-tag>-</a-tag>
@ -222,11 +209,8 @@
<tr> <tr>
<td>{{ i18n "pages.inbounds.updatedAt" }}</td> <td>{{ i18n "pages.inbounds.updatedAt" }}</td>
<td> <td>
<template <template v-if="infoModal.clientSettings && infoModal.clientSettings.updated_at">
v-if="infoModal.clientSettings && infoModal.clientSettings.updated_at"> <a-tag>[[ IntlUtil.formatDate(infoModal.clientSettings.updated_at) ]]</a-tag>
<a-tag>[[
IntlUtil.formatDate(infoModal.clientSettings.updated_at)
]]</a-tag>
</template> </template>
<template v-else> <template v-else>
<a-tag>-</a-tag> <a-tag>-</a-tag>
@ -236,17 +220,14 @@
<tr> <tr>
<td>{{ i18n "lastOnline" }}</td> <td>{{ i18n "lastOnline" }}</td>
<td> <td>
<a-tag>[[ app.formatLastOnline(infoModal.clientSettings && <a-tag>[[ app.formatLastOnline(infoModal.clientSettings && infoModal.clientSettings.email ? infoModal.clientSettings.email : '') ]]</a-tag>
infoModal.clientSettings.email ?
infoModal.clientSettings.email : '') ]]</a-tag>
</td> </td>
</tr> </tr>
<tr v-if="infoModal.clientSettings.comment"> <tr v-if="infoModal.clientSettings.comment">
<td>{{ i18n "comment" }}</td> <td>{{ i18n "comment" }}</td>
<td> <td>
<a-tooltip :title="[[ infoModal.clientSettings.comment ]]"> <a-tooltip :title="[[ infoModal.clientSettings.comment ]]">
<a-tag class="info-large-tag">[[ <a-tag class="info-large-tag">[[ infoModal.clientSettings.comment ]]</a-tag>
infoModal.clientSettings.comment ]]</a-tag>
</a-tooltip> </a-tooltip>
</td> </td>
</tr> </tr>
@ -256,40 +237,21 @@
<a-tag>[[ infoModal.clientSettings.limitIp ]]</a-tag> <a-tag>[[ infoModal.clientSettings.limitIp ]]</a-tag>
</td> </td>
</tr> </tr>
<tr <tr v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0">
v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0">
<td>{{ i18n "pages.inbounds.IPLimitlog" }}</td> <td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
<td> <td>
<div <a-tag>[[ infoModal.clientIps ]]</a-tag>
style="max-height: 150px; overflow-y: auto; text-align: left;"> <a-icon type="sync" :spin="refreshing" @click="refreshIPs" :style="{ margin: '0 5px' }"></a-icon>
<div <a-tooltip :title="[[ dbInbound.address ]]">
v-if="infoModal.clientIpsArray && infoModal.clientIpsArray.length > 0">
<a-tag
v-for="(ipInfo, idx) in infoModal.clientIpsArray"
:key="idx"
color="blue"
style="margin: 2px 0; display: block; font-family: monospace; font-size: 11px;">
[[ formatIpInfo(ipInfo) ]]
</a-tag>
</div>
<a-tag v-else>[[ infoModal.clientIps || 'No IP Record'
]]</a-tag>
</div>
<div style="margin-top: 5px;">
<a-icon type="sync" :spin="refreshing" @click="refreshIPs"
:style="{ margin: '0 5px' }"></a-icon>
<a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span> <span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
</template> </template>
<a-icon type="delete" @click="clearClientIps"></a-icon> <a-icon type="delete" @click="clearClientIps"></a-icon>
</a-tooltip> </a-tooltip>
</div>
</td> </td>
</tr> </tr>
</table> </table>
<table <table :style="{ display: 'inline-table', marginBlock: '10px', width: '100%', textAlign: 'center' }">
:style="{ display: 'inline-table', marginBlock: '10px', width: '100%', textAlign: 'center' }">
<tr> <tr>
<th>{{ i18n "remained" }}</th> <th>{{ i18n "remained" }}</th>
<th>{{ i18n "pages.inbounds.totalFlow" }}</th> <th>{{ i18n "pages.inbounds.totalFlow" }}</th>
@ -297,73 +259,51 @@
</tr> </tr>
<tr> <tr>
<td> <td>
<a-tag <a-tag v-if="infoModal.clientStats && infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)"> [[ getRemStats() ]] </a-tag>
v-if="infoModal.clientStats && infoModal.clientSettings.totalGB > 0"
:color="statsColor(infoModal.clientStats)"> [[ getRemStats()
]] </a-tag>
</td> </td>
<td> <td>
<a-tag v-if="infoModal.clientSettings.totalGB > 0" <a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)"> [[ SizeFormatter.sizeFormat(infoModal.clientSettings.totalGB) ]] </a-tag>
:color="statsColor(infoModal.clientStats)"> [[
SizeFormatter.sizeFormat(infoModal.clientSettings.totalGB) ]]
</a-tag>
<a-tag v-else color="purple" class="infinite-tag"> <a-tag v-else color="purple" class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
fill="currentColor"> <path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
<path
d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
fill="currentColor"></path>
</svg> </svg>
</a-tag> </a-tag>
</td> </td>
<td> <td>
<template v-if="infoModal.clientSettings.expiryTime > 0"> <template v-if="infoModal.clientSettings.expiryTime > 0">
<a-tag <a-tag :color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)">
:color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)"> [[ IntlUtil.formatDate(infoModal.clientSettings.expiryTime) ]]
[[ IntlUtil.formatDate(infoModal.clientSettings.expiryTime)
]]
</a-tag> </a-tag>
</template> </template>
<a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" <a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="green">[[ infoModal.clientSettings.expiryTime / -86400000 ]] {{ i18n "pages.client.days" }}
color="green">[[ infoModal.clientSettings.expiryTime /
-86400000 ]] {{ i18n "pages.client.days" }}
</a-tag> </a-tag>
<a-tag v-else color="purple" class="infinite-tag"> <a-tag v-else color="purple" class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
fill="currentColor"> <path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
<path
d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
fill="currentColor"></path>
</svg> </svg>
</a-tag> </a-tag>
</td> </td>
</tr> </tr>
</table> </table>
<template <template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
v-if="app.subSettings.enable && infoModal.clientSettings.subId">
<a-divider>Subscription URL</a-divider> <a-divider>Subscription URL</a-divider>
<tr-info-row class="tr-info-row"> <tr-info-row class="tr-info-row">
<tr-info-title class="tr-info-title"> <tr-info-title class="tr-info-title">
<a-tag color="purple">Subscription Link</a-tag> <a-tag color="purple">Subscription Link</a-tag>
<a-tooltip title='{{ i18n "copy" }}'> <a-tooltip title='{{ i18n "copy" }}'>
<a-button size="small" icon="snippets" <a-button size="small" icon="snippets" @click="copy(infoModal.subLink)"></a-button>
@click="copy(infoModal.subLink)"></a-button>
</a-tooltip> </a-tooltip>
</tr-info-title> </tr-info-title>
<a :href="[[ infoModal.subLink ]]" target="_blank">[[ <a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a>
infoModal.subLink ]]</a>
</tr-info-row> </tr-info-row>
<tr-info-row class="tr-info-row" <tr-info-row class="tr-info-row" v-if="app.subSettings.subJsonEnable">
v-if="app.subSettings.subJsonEnable">
<tr-info-title class="tr-info-title"> <tr-info-title class="tr-info-title">
<a-tag color="purple">Json Link</a-tag> <a-tag color="purple">Json Link</a-tag>
<a-tooltip title='{{ i18n "copy" }}'> <a-tooltip title='{{ i18n "copy" }}'>
<a-button size="small" icon="snippets" <a-button size="small" icon="snippets" @click="copy(infoModal.subJsonLink)"></a-button>
@click="copy(infoModal.subJsonLink)"></a-button>
</a-tooltip> </a-tooltip>
</tr-info-title> </tr-info-title>
<a :href="[[ infoModal.subJsonLink ]]" target="_blank">[[ <a :href="[[ infoModal.subJsonLink ]]" target="_blank">[[ infoModal.subJsonLink ]]</a>
infoModal.subJsonLink ]]</a>
</tr-info-row> </tr-info-row>
</template> </template>
<template v-if="app.tgBotEnable && infoModal.clientSettings.tgId"> <template v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
@ -372,22 +312,18 @@
<tr-info-title class="tr-info-title"> <tr-info-title class="tr-info-title">
<a-tag color="blue">[[ infoModal.clientSettings.tgId ]]</a-tag> <a-tag color="blue">[[ infoModal.clientSettings.tgId ]]</a-tag>
<a-tooltip title='{{ i18n "copy" }}'> <a-tooltip title='{{ i18n "copy" }}'>
<a-button size="small" icon="snippets" <a-button size="small" icon="snippets" @click="copy(infoModal.clientSettings.tgId)"></a-button>
@click="copy(infoModal.clientSettings.tgId)"></a-button>
</a-tooltip> </a-tooltip>
</tr-info-title> </tr-info-title>
</tr-info-row> </tr-info-row>
</template> </template>
<template v-if="dbInbound.hasLink()"> <template v-if="dbInbound.hasLink()">
<a-divider>URL</a-divider> <a-divider>URL</a-divider>
<tr-info-row v-for="(link,index) in infoModal.links" <tr-info-row v-for="(link,index) in infoModal.links" class="tr-info-row">
class="tr-info-row">
<tr-info-title class="tr-info-title"> <tr-info-title class="tr-info-title">
<a-tag class="tr-info-tag" color="green">[[ link.remark <a-tag class="tr-info-tag" color="green">[[ link.remark ]]</a-tag>
]]</a-tag>
<a-tooltip title='{{ i18n "copy" }}'> <a-tooltip title='{{ i18n "copy" }}'>
<a-button :style="{ minWidth: '24px' }" size="small" <a-button :style="{ minWidth: '24px' }" size="small" icon="snippets" @click="copy(link.link)"></a-button>
icon="snippets" @click="copy(link.link)"></a-button>
</a-tooltip> </a-tooltip>
</tr-info-title> </tr-info-title>
<code>[[ link.link ]]</code> <code>[[ link.link ]]</code>
@ -397,21 +333,17 @@
<template v-else> <template v-else>
<template v-if="dbInbound.isSS && !inbound.isSSMultiUser"> <template v-if="dbInbound.isSS && !inbound.isSSMultiUser">
<a-divider>URL</a-divider> <a-divider>URL</a-divider>
<tr-info-row v-for="(link,index) in infoModal.links" <tr-info-row v-for="(link,index) in infoModal.links" class="tr-info-row">
class="tr-info-row">
<tr-info-title class="tr-info-title"> <tr-info-title class="tr-info-title">
<a-tag class="tr-info-tag" color="green">[[ link.remark <a-tag class="tr-info-tag" color="green">[[ link.remark ]]</a-tag>
]]</a-tag>
<a-tooltip title='{{ i18n "copy" }}'> <a-tooltip title='{{ i18n "copy" }}'>
<a-button :style="{ minWidth: '24px' }" size="small" <a-button :style="{ minWidth: '24px' }" size="small" icon="snippets" @click="copy(link.link)"></a-button>
icon="snippets" @click="copy(link.link)"></a-button>
</a-tooltip> </a-tooltip>
</tr-info-title> </tr-info-title>
<code>[[ link.link ]]</code> <code>[[ link.link ]]</code>
</tr-info-row> </tr-info-row>
</template> </template>
<table v-if="inbound.protocol == Protocols.TUNNEL" <table v-if="inbound.protocol == Protocols.TUNNEL" class="tr-info-table">
class="tr-info-table">
<tr> <tr>
<th>{{ i18n "pages.inbounds.targetAddress" }}</th> <th>{{ i18n "pages.inbounds.targetAddress" }}</th>
<th>{{ i18n "pages.inbounds.destinationPort" }}</th> <th>{{ i18n "pages.inbounds.destinationPort" }}</th>
@ -429,8 +361,7 @@
<a-tag color="green">[[ inbound.settings.network ]]</a-tag> <a-tag color="green">[[ inbound.settings.network ]]</a-tag>
</td> </td>
<td> <td>
<a-tag color="green">[[ inbound.settings.followRedirect <a-tag color="green">[[ inbound.settings.followRedirect ]]</a-tag>
]]</a-tag>
</td> </td>
</tr> </tr>
</table> </table>
@ -533,20 +464,13 @@
<tr-info-title class="tr-info-title"> <tr-info-title class="tr-info-title">
<a-tag color="blue">Config</a-tag> <a-tag color="blue">Config</a-tag>
<a-tooltip title='{{ i18n "copy" }}'> <a-tooltip title='{{ i18n "copy" }}'>
<a-button :style="{ minWidth: '24px' }" size="small" <a-button :style="{ minWidth: '24px' }" size="small" icon="snippets" @click="copy(infoModal.links[index])"></a-button>
icon="snippets"
@click="copy(infoModal.links[index])"></a-button>
</a-tooltip> </a-tooltip>
<a-tooltip title='{{ i18n "download" }}'> <a-tooltip title='{{ i18n "download" }}'>
<a-button :style="{ minWidth: '24px' }" size="small" <a-button :style="{ minWidth: '24px' }" size="small" icon="download" @click="FileManager.downloadTextFile(infoModal.links[index], `peer-${index + 1}.conf`)"></a-button>
icon="download"
@click="FileManager.downloadTextFile(infoModal.links[index], `peer-${index + 1}.conf`)"></a-button>
</a-tooltip> </a-tooltip>
</tr-info-title> </tr-info-title>
<div <div v-html="infoModal.links[index].replaceAll(`\n`,`<br />`)" :style="{ borderRadius: '1rem', padding: '0.5rem' }" class="client-table-odd-row">
v-html="infoModal.links[index].replaceAll(`\n`,`<br />`)"
:style="{ borderRadius: '1rem', padding: '0.5rem' }"
class="client-table-odd-row">
</div> </div>
</tr-info-row> </tr-info-row>
</td> </td>
@ -558,74 +482,13 @@
<script> <script>
function refreshIPs(email) { function refreshIPs(email) {
return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => { return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
if (!msg.success) { if (msg.success) {
return { text: 'No IP Record', array: [] };
}
const formatIpRecord = (record) => {
if (record == null) {
return '';
}
if (typeof record === 'string' || typeof record === 'number') {
return String(record);
}
const ip = record.ip || record.IP || '';
const timestamp = record.timestamp || record.Timestamp || 0;
if (!ip) {
return String(record);
}
if (!timestamp) {
return String(ip);
}
const date = new Date(Number(timestamp) * 1000);
const timeStr = date
.toLocaleString('en-GB', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
})
.replace(',', '');
return `${ip} (${timeStr})`;
};
try { try {
let ips = msg.obj; return JSON.parse(msg.obj).join(', ');
// If msg.obj is a string, try to parse it
if (typeof ips === 'string') {
try {
ips = JSON.parse(ips);
} catch (e) { } catch (e) {
return { text: String(ips), array: [String(ips)] }; return msg.obj;
} }
} }
// Normalize single object response to array
if (ips && !Array.isArray(ips) && typeof ips === 'object') {
ips = [ips];
}
// New format or object array
if (Array.isArray(ips) && ips.length > 0 && typeof ips[0] === 'object') {
const result = ips.map((item) => formatIpRecord(item)).filter(Boolean);
return { text: result.join(' | '), array: result };
}
// Old format - simple array of IPs
if (Array.isArray(ips) && ips.length > 0) {
const result = ips.map((ip) => String(ip));
return { text: result.join(', '), array: result };
}
// Fallback for any other format
return { text: String(ips), array: [String(ips)] };
} catch (e) {
return { text: 'Error loading IPs', array: [] };
}
}); });
} }
@ -643,7 +506,6 @@
subLink: '', subLink: '',
subJsonLink: '', subJsonLink: '',
clientIps: '', clientIps: '',
clientIpsArray: [],
show(dbInbound, index) { show(dbInbound, index) {
this.index = index; this.index = index;
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
@ -661,9 +523,8 @@
].includes(this.inbound.protocol) ].includes(this.inbound.protocol)
) { ) {
if (app.ipLimitEnable && this.clientSettings.limitIp) { if (app.ipLimitEnable && this.clientSettings.limitIp) {
refreshIPs(this.clientStats.email).then((result) => { refreshIPs(this.clientStats.email).then((ips) => {
this.clientIps = result.text; this.clientIps = ips;
this.clientIpsArray = result.array;
}) })
} }
} }
@ -734,35 +595,6 @@
}, },
}, },
methods: { methods: {
formatIpInfo(ipInfo) {
if (ipInfo == null) {
return '';
}
if (typeof ipInfo === 'string' || typeof ipInfo === 'number') {
return String(ipInfo);
}
const ip = ipInfo.ip || ipInfo.IP || '';
const timestamp = ipInfo.timestamp || ipInfo.Timestamp || 0;
if (!ip) {
return String(ipInfo);
}
if (!timestamp) {
return String(ip);
}
const date = new Date(Number(timestamp) * 1000);
const timeStr = date
.toLocaleString('en-GB', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
})
.replace(',', '');
return `${ip} (${timeStr})`;
},
copy(content) { copy(content) {
ClipboardManager ClipboardManager
.copyText(content) .copyText(content)
@ -780,9 +612,8 @@
refreshIPs() { refreshIPs() {
this.refreshing = true; this.refreshing = true;
refreshIPs(this.infoModal.clientStats.email) refreshIPs(this.infoModal.clientStats.email)
.then((result) => { .then((ips) => {
this.infoModal.clientIps = result.text; this.infoModal.clientIps = ips;
this.infoModal.clientIpsArray = result.array;
}) })
.finally(() => { .finally(() => {
this.refreshing = false; this.refreshing = false;
@ -795,7 +626,6 @@
return; return;
} }
this.infoModal.clientIps = 'No IP Record'; this.infoModal.clientIps = 'No IP Record';
this.infoModal.clientIpsArray = [];
}) })
.catch(() => {}); .catch(() => {});
}, },

View file

@ -219,14 +219,14 @@
rule = {}; rule = {};
newRule = {}; newRule = {};
rule.type = "field"; rule.type = "field";
rule.domain = value.domain.length > 0 ? value.domain.split(',').map(s => s.trim()) : []; rule.domain = value.domain.length > 0 ? value.domain.split(',') : [];
rule.ip = value.ip.length > 0 ? value.ip.split(',').map(s => s.trim()) : []; rule.ip = value.ip.length > 0 ? value.ip.split(',') : [];
rule.port = value.port; rule.port = value.port;
rule.sourcePort = value.sourcePort; rule.sourcePort = value.sourcePort;
rule.vlessRoute = value.vlessRoute; rule.vlessRoute = value.vlessRoute;
rule.network = value.network; rule.network = value.network;
rule.sourceIP = value.sourceIP.length > 0 ? value.sourceIP.split(',').map(s => s.trim()) : []; rule.sourceIP = value.sourceIP.length > 0 ? value.sourceIP.split(',') : [];
rule.user = value.user.length > 0 ? value.user.split(',').map(s => s.trim()) : []; rule.user = value.user.length > 0 ? value.user.split(',') : [];
rule.inboundTag = value.inboundTag; rule.inboundTag = value.inboundTag;
rule.protocol = value.protocol; rule.protocol = value.protocol;
rule.attrs = Object.fromEntries(value.attrs); rule.attrs = Object.fromEntries(value.attrs);

View file

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

View file

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

View file

@ -4,22 +4,18 @@
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" :style="{ textAlign: 'center' }"> <a-alert type="warning" :style="{ textAlign: 'center' }">
<template slot="message"> <template slot="message">
<a-icon type="exclamation-circle" theme="filled" <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
:style="{ color: '#FFA031' }"></a-icon>
<span>{{ i18n "pages.xray.generalConfigsDesc" }}</span> <span>{{ i18n "pages.xray.generalConfigsDesc" }}</span>
</template> </template>
</a-alert> </a-alert>
</a-row> </a-row>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.FreedomStrategy" }}</template> <template #title>{{ i18n "pages.xray.FreedomStrategy" }}</template>
<template #description>{{ i18n "pages.xray.FreedomStrategyDesc" <template #description>{{ i18n "pages.xray.FreedomStrategyDesc" }}</template>
}}</template>
<template #control> <template #control>
<a-select v-model="freedomStrategy" <a-select v-model="freedomStrategy" :dropdown-class-name="themeSwitcher.currentTheme"
:dropdown-class-name="themeSwitcher.currentTheme"
:style="{ width: '100%' }"> :style="{ width: '100%' }">
<a-select-option v-for="s in OutboundDomainStrategies" <a-select-option v-for="s in OutboundDomainStrategies" :value="s">
:value="s">
<span>[[ s ]]</span> <span>[[ s ]]</span>
</a-select-option> </a-select-option>
</a-select> </a-select>
@ -27,63 +23,42 @@
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.RoutingStrategy" }}</template> <template #title>{{ i18n "pages.xray.RoutingStrategy" }}</template>
<template #description>{{ i18n "pages.xray.RoutingStrategyDesc" <template #description>{{ i18n "pages.xray.RoutingStrategyDesc" }}</template>
}}</template>
<template #control> <template #control>
<a-select v-model="routingStrategy" <a-select v-model="routingStrategy" :dropdown-class-name="themeSwitcher.currentTheme"
:dropdown-class-name="themeSwitcher.currentTheme"
:style="{ width: '100%' }"> :style="{ width: '100%' }">
<a-select-option v-for="s in routingDomainStrategies" <a-select-option v-for="s in routingDomainStrategies" :value="s">
:value="s">
<span>[[ s ]]</span> <span>[[ s ]]</span>
</a-select-option> </a-select-option>
</a-select> </a-select>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.outboundTestUrl" }}</template>
<template #description>{{ i18n "pages.xray.outboundTestUrlDesc"
}}</template>
<template #control>
<a-input v-model="outboundTestUrl"
:placeholder="'https://www.google.com/generate_204'"
:style="{ width: '100%' }"></a-input>
</template>
</a-setting-list-item>
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel key="2" header='{{ i18n "pages.xray.statistics" }}'> <a-collapse-panel key="2" header='{{ i18n "pages.xray.statistics" }}'>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.statsInboundUplink" <template #title>{{ i18n "pages.xray.statsInboundUplink" }}</template>
}}</template> <template #description>{{ i18n "pages.xray.statsInboundUplinkDesc" }}</template>
<template #description>{{ i18n "pages.xray.statsInboundUplinkDesc"
}}</template>
<template #control> <template #control>
<a-switch v-model="statsInboundUplink"></a-switch> <a-switch v-model="statsInboundUplink"></a-switch>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.statsInboundDownlink" <template #title>{{ i18n "pages.xray.statsInboundDownlink" }}</template>
}}</template> <template #description>{{ i18n "pages.xray.statsInboundDownlinkDesc" }}</template>
<template #description>{{ i18n "pages.xray.statsInboundDownlinkDesc"
}}</template>
<template #control> <template #control>
<a-switch v-model="statsInboundDownlink"></a-switch> <a-switch v-model="statsInboundDownlink"></a-switch>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.statsOutboundUplink" <template #title>{{ i18n "pages.xray.statsOutboundUplink" }}</template>
}}</template> <template #description>{{ i18n "pages.xray.statsOutboundUplinkDesc" }}</template>
<template #description>{{ i18n "pages.xray.statsOutboundUplinkDesc"
}}</template>
<template #control> <template #control>
<a-switch v-model="statsOutboundUplink"></a-switch> <a-switch v-model="statsOutboundUplink"></a-switch>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.statsOutboundDownlink" <template #title>{{ i18n "pages.xray.statsOutboundDownlink" }}</template>
}}</template> <template #description>{{ i18n "pages.xray.statsOutboundDownlinkDesc" }}</template>
<template #description>{{ i18n
"pages.xray.statsOutboundDownlinkDesc" }}</template>
<template #control> <template #control>
<a-switch v-model="statsOutboundDownlink"></a-switch> <a-switch v-model="statsOutboundDownlink"></a-switch>
</template> </template>
@ -93,20 +68,16 @@
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" :style="{ textAlign: 'center' }"> <a-alert type="warning" :style="{ textAlign: 'center' }">
<template slot="message"> <template slot="message">
<a-icon type="exclamation-circle" theme="filled" <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
:style="{ color: '#FFA031' }"></a-icon>
<span>{{ i18n "pages.xray.logConfigsDesc" }}</span> <span>{{ i18n "pages.xray.logConfigsDesc" }}</span>
</template> </template>
</a-alert> </a-alert>
</a-row> </a-row>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.logLevel" }}</template> <template #title>{{ i18n "pages.xray.logLevel" }}</template>
<template #description>{{ i18n "pages.xray.logLevelDesc" <template #description>{{ i18n "pages.xray.logLevelDesc" }}</template>
}}</template>
<template #control> <template #control>
<a-select v-model="logLevel" <a-select v-model="logLevel" :dropdown-class-name="themeSwitcher.currentTheme" :style="{ width: '100%' }">
:dropdown-class-name="themeSwitcher.currentTheme"
:style="{ width: '100%' }">
<a-select-option v-for="s in log.loglevel" :value="s"> <a-select-option v-for="s in log.loglevel" :value="s">
<span>[[ s ]]</span> <span>[[ s ]]</span>
</a-select-option> </a-select-option>
@ -115,13 +86,10 @@
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.accessLog" }}</template> <template #title>{{ i18n "pages.xray.accessLog" }}</template>
<template #description>{{ i18n "pages.xray.accessLogDesc" <template #description>{{ i18n "pages.xray.accessLogDesc" }}</template>
}}</template>
<template #control> <template #control>
<a-select v-model="accessLog" <a-select v-model="accessLog" :dropdown-class-name="themeSwitcher.currentTheme" :style="{ width: '100%' }">
:dropdown-class-name="themeSwitcher.currentTheme" <a-select-option value=''>
:style="{ width: '100%' }">
<a-select-option value>
<span>Empty</span> <span>Empty</span>
</a-select-option> </a-select-option>
<a-select-option v-for="s in log.access" :value="s"> <a-select-option v-for="s in log.access" :value="s">
@ -132,13 +100,10 @@
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.errorLog" }}</template> <template #title>{{ i18n "pages.xray.errorLog" }}</template>
<template #description>{{ i18n "pages.xray.errorLogDesc" <template #description>{{ i18n "pages.xray.errorLogDesc" }}</template>
}}</template>
<template #control> <template #control>
<a-select v-model="errorLog" <a-select v-model="errorLog" :dropdown-class-name="themeSwitcher.currentTheme" :style="{ width: '100%' }">
:dropdown-class-name="themeSwitcher.currentTheme" <a-select-option value=''>
:style="{ width: '100%' }">
<a-select-option value>
<span>Empty</span> <span>Empty</span>
</a-select-option> </a-select-option>
<a-select-option v-for="s in log.error" :value="s"> <a-select-option v-for="s in log.error" :value="s">
@ -149,13 +114,11 @@
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.maskAddress" }}</template> <template #title>{{ i18n "pages.xray.maskAddress" }}</template>
<template #description>{{ i18n "pages.xray.maskAddressDesc" <template #description>{{ i18n "pages.xray.maskAddressDesc" }}</template>
}}</template>
<template #control> <template #control>
<a-select v-model="maskAddressLog" <a-select v-model="maskAddressLog" :dropdown-class-name="themeSwitcher.currentTheme"
:dropdown-class-name="themeSwitcher.currentTheme"
:style="{ width: '100%' }"> :style="{ width: '100%' }">
<a-select-option value> <a-select-option value=''>
<span>Empty</span> <span>Empty</span>
</a-select-option> </a-select-option>
<a-select-option v-for="s in log.maskAddress" :value="s"> <a-select-option v-for="s in log.maskAddress" :value="s">
@ -176,8 +139,7 @@
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" :style="{ textAlign: 'center' }"> <a-alert type="warning" :style="{ textAlign: 'center' }">
<template slot="message"> <template slot="message">
<a-icon type="exclamation-circle" theme="filled" <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
:style="{ color: '#FFA031' }"></a-icon>
<span>{{ i18n "pages.xray.blockConfigsDesc" }}</span> <span>{{ i18n "pages.xray.blockConfigsDesc" }}</span>
</template> </template>
</a-alert> </a-alert>
@ -191,21 +153,17 @@
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" :style="{ textAlign: 'center' }"> <a-alert type="warning" :style="{ textAlign: 'center' }">
<template slot="message"> <template slot="message">
<a-icon type="exclamation-circle" theme="filled" <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
:style="{ color: '#FFA031' }"></a-icon> <span>{{ i18n "pages.xray.blockConnectionsConfigsDesc" }}</span>
<span>{{ i18n "pages.xray.blockConnectionsConfigsDesc"
}}</span>
</template> </template>
</a-alert> </a-alert>
</a-row> </a-row>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.blockips" }}</template> <template #title>{{ i18n "pages.xray.blockips" }}</template>
<template #control> <template #control>
<a-select mode="tags" v-model="blockedIPs" <a-select mode="tags" v-model="blockedIPs" :style="{ width: '100%' }"
:style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label" <a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.IPsOptions">
v-for="p in settingsData.IPsOptions">
<span>[[ p.label ]]</span> <span>[[ p.label ]]</span>
</a-select-option> </a-select-option>
</a-select> </a-select>
@ -214,35 +172,28 @@
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.blockdomains" }}</template> <template #title>{{ i18n "pages.xray.blockdomains" }}</template>
<template #control> <template #control>
<a-select mode="tags" v-model="blockedDomains" <a-select mode="tags" v-model="blockedDomains" :style="{ width: '100%' }"
:style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label" <a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.BlockDomainsOptions">
v-for="p in settingsData.BlockDomainsOptions">
<span>[[ p.label ]]</span> <span>[[ p.label ]]</span>
</a-select-option> </a-select-option>
</a-select> </a-select>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" <a-alert type="warning" :style="{ textAlign: 'center', marginTop: '20px' }">
:style="{ textAlign: 'center', marginTop: '20px' }">
<template slot="message"> <template slot="message">
<a-icon type="exclamation-circle" theme="filled" <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
:style="{ color: '#FFA031' }"></a-icon> <span>{{ i18n "pages.xray.directConnectionsConfigsDesc" }}</span>
<span>{{ i18n "pages.xray.directConnectionsConfigsDesc"
}}</span>
</template> </template>
</a-alert> </a-alert>
</a-row> </a-row>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.directips" }}</template> <template #title>{{ i18n "pages.xray.directips" }}</template>
<template #control> <template #control>
<a-select mode="tags" :style="{ width: '100%' }" <a-select mode="tags" :style="{ width: '100%' }" v-model="directIPs"
v-model="directIPs"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label" <a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.IPsOptions">
v-for="p in settingsData.IPsOptions">
<span>[[ p.label ]]</span> <span>[[ p.label ]]</span>
</a-select-option> </a-select-option>
</a-select> </a-select>
@ -251,22 +202,18 @@
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.directdomains" }}</template> <template #title>{{ i18n "pages.xray.directdomains" }}</template>
<template #control> <template #control>
<a-select mode="tags" :style="{ width: '100%' }" <a-select mode="tags" :style="{ width: '100%' }" v-model="directDomains"
v-model="directDomains"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label" <a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.DomainsOptions">
v-for="p in settingsData.DomainsOptions">
<span>[[ p.label ]]</span> <span>[[ p.label ]]</span>
</a-select-option> </a-select-option>
</a-select> </a-select>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" <a-alert type="warning" :style="{ textAlign: 'center', marginTop: '20px' }">
:style="{ textAlign: 'center', marginTop: '20px' }">
<template slot="message"> <template slot="message">
<a-icon type="exclamation-circle" theme="filled" <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
:style="{ color: '#FFA031' }"></a-icon>
<span>{{ i18n "pages.xray.ipv4RoutingDesc" }}</span> <span>{{ i18n "pages.xray.ipv4RoutingDesc" }}</span>
</template> </template>
</a-alert> </a-alert>
@ -274,22 +221,18 @@
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.ipv4Routing" }}</template> <template #title>{{ i18n "pages.xray.ipv4Routing" }}</template>
<template #control> <template #control>
<a-select mode="tags" :style="{ width: '100%' }" <a-select mode="tags" :style="{ width: '100%' }" v-model="ipv4Domains"
v-model="ipv4Domains"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label" <a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.ServicesOptions">
v-for="p in settingsData.ServicesOptions">
<span>[[ p.label ]]</span> <span>[[ p.label ]]</span>
</a-select-option> </a-select-option>
</a-select> </a-select>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" <a-alert type="warning" :style="{ textAlign: 'center', marginTop: '20px' }">
:style="{ textAlign: 'center', marginTop: '20px' }">
<template slot="message"> <template slot="message">
<a-icon type="exclamation-circle" theme="filled" <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
:style="{ color: '#FFA031' }"></a-icon>
{{ i18n "pages.xray.warpRoutingDesc" }} {{ i18n "pages.xray.warpRoutingDesc" }}
</template> </template>
</a-alert> </a-alert>
@ -298,24 +241,20 @@
<template #title>{{ i18n "pages.xray.warpRouting" }}</template> <template #title>{{ i18n "pages.xray.warpRouting" }}</template>
<template #control> <template #control>
<template v-if="WarpExist"> <template v-if="WarpExist">
<a-select mode="tags" :style="{ width: '100%' }" <a-select mode="tags" :style="{ width: '100%' }" v-model="warpDomains"
v-model="warpDomains"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label" <a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.ServicesOptions">
v-for="p in settingsData.ServicesOptions">
<span>[[ p.label ]]</span> <span>[[ p.label ]]</span>
</a-select-option> </a-select-option>
</a-select> </a-select>
</template> </template>
<template v-else> <template v-else>
<a-button type="primary" icon="cloud" <a-button type="primary" icon="cloud" @click="showWarp()">WARP</a-button>
@click="showWarp()">WARP</a-button>
</template> </template>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel key="6" <a-collapse-panel key="6" header='{{ i18n "pages.settings.resetDefaultConfig"}}'>
header='{{ i18n "pages.settings.resetDefaultConfig"}}'>
<a-space direction="horizontal" :style="{ padding: '0 20px' }"> <a-space direction="horizontal" :style="{ padding: '0 20px' }">
<a-button type="danger" @click="resetXrayConfigToDefault"> <a-button type="danger" @click="resetXrayConfigToDefault">
<span>{{ i18n "pages.settings.resetDefaultConfig" }}</span> <span>{{ i18n "pages.settings.resetDefaultConfig" }}</span>

View file

@ -3,23 +3,18 @@
<a-row> <a-row>
<a-col :xs="12" :sm="12" :lg="12"> <a-col :xs="12" :sm="12" :lg="12">
<a-space direction="horizontal" size="small"> <a-space direction="horizontal" size="small">
<a-button type="primary" icon="plus" @click="addOutbound"> <a-button type="primary" icon="plus" @click="addOutbound()">
<span v-if="!isMobile">{{ i18n {{ i18n "pages.xray.outbound.addOutbound" }}
"pages.xray.outbound.addOutbound" }}</span>
</a-button> </a-button>
<a-button type="primary" icon="cloud" <a-button type="primary" icon="cloud" @click="showWarp()">WARP</a-button>
@click="showWarp()">WARP</a-button>
</a-space> </a-space>
</a-col> </a-col>
<a-col :xs="12" :sm="12" :lg="12" :style="{ textAlign: 'right' }"> <a-col :xs="12" :sm="12" :lg="12" :style="{ textAlign: 'right' }">
<a-button-group> <a-button-group>
<a-button icon="sync" @click="refreshOutboundTraffic()" <a-button icon="sync" @click="refreshOutboundTraffic()" :loading="refreshing"></a-button>
:loading="refreshing"></a-button> <a-popconfirm placement="topRight" @confirm="resetOutboundTraffic(-1)"
<a-popconfirm placement="topRight"
@confirm="resetOutboundTraffic(-1)"
title='{{ i18n "pages.inbounds.resetTrafficContent"}}' title='{{ i18n "pages.inbounds.resetTrafficContent"}}'
:overlay-class-name="themeSwitcher.currentTheme" :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}'
ok-text='{{ i18n "reset"}}'
cancel-text='{{ i18n "cancel"}}'> cancel-text='{{ i18n "cancel"}}'>
<a-icon slot="icon" type="question-circle-o" <a-icon slot="icon" type="question-circle-o"
:style="{ color: themeSwitcher.isDarkTheme ? '#008771' : '#008771' }"></a-icon> :style="{ color: themeSwitcher.isDarkTheme ? '#008771' : '#008771' }"></a-icon>
@ -28,10 +23,8 @@
</a-button-group> </a-button-group>
</a-col> </a-col>
</a-row> </a-row>
<a-table :columns="outboundColumns" bordered :row-key="r => r.key" <a-table :columns="outboundColumns" bordered :row-key="r => r.key" :data-source="outboundData"
:data-source="outboundData" :scroll="isMobile ? {} : { x: 800 }" :pagination="false" :indent-size="0"
:scroll="isMobile ? {} : { x: 800 }" :pagination="false"
:indent-size="0"
:locale='{ filterConfirm: `{{ i18n "confirm" }}`, filterReset: `{{ i18n "reset" }}` }'> :locale='{ filterConfirm: `{{ i18n "confirm" }}`, filterReset: `{{ i18n "reset" }}` }'>
<template slot="action" slot-scope="text, outbound, index"> <template slot="action" slot-scope="text, outbound, index">
<span>[[ index+1 ]]</span> <span>[[ index+1 ]]</span>
@ -39,8 +32,7 @@
<a-icon @click="e => e.preventDefault()" type="more" <a-icon @click="e => e.preventDefault()" type="more"
:style="{ fontSize: '16px', textDecoration: 'bold' }"></a-icon> :style="{ fontSize: '16px', textDecoration: 'bold' }"></a-icon>
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme"> <a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
<a-menu-item v-if="index>0" <a-menu-item v-if="index>0" @click="setFirstOutbound(index)">
@click="setFirstOutbound(index)">
<a-icon type="vertical-align-top"></a-icon> <a-icon type="vertical-align-top"></a-icon>
<span>{{ i18n "pages.xray.rules.first"}}</span> <span>{{ i18n "pages.xray.rules.first"}}</span>
</a-menu-item> </a-menu-item>
@ -64,64 +56,21 @@
</a-dropdown> </a-dropdown>
</template> </template>
<template slot="address" slot-scope="text, outbound, index"> <template slot="address" slot-scope="text, outbound, index">
<p :style="{ margin: '0 5px' }" <p :style="{ margin: '0 5px' }" v-for="addr in findOutboundAddress(outbound)">[[ addr ]]</p>
v-for="addr in findOutboundAddress(outbound)">[[ addr ]]</p>
</template> </template>
<template slot="protocol" slot-scope="text, outbound, index"> <template slot="protocol" slot-scope="text, outbound, index">
<a-tag :style="{ margin: '0' }" color="purple">[[ outbound.protocol <a-tag :style="{ margin: '0' }" color="purple">[[ outbound.protocol ]]</a-tag>
]]</a-tag>
<template <template
v-if="[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)"> v-if="[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
<a-tag :style="{ margin: '0' }" color="blue">[[ <a-tag :style="{ margin: '0' }" color="blue">[[ outbound.streamSettings.network ]]</a-tag>
outbound.streamSettings.network ]]</a-tag> <a-tag :style="{ margin: '0' }" v-if="outbound.streamSettings.security=='tls'" color="green">tls</a-tag>
<a-tag :style="{ margin: '0' }" <a-tag :style="{ margin: '0' }" v-if="outbound.streamSettings.security=='reality'"
v-if="outbound.streamSettings.security=='tls'"
color="green">tls</a-tag>
<a-tag :style="{ margin: '0' }"
v-if="outbound.streamSettings.security=='reality'"
color="green">reality</a-tag> color="green">reality</a-tag>
</template> </template>
</template> </template>
<template slot="traffic" slot-scope="text, outbound, index"> <template slot="traffic" slot-scope="text, outbound, index">
<a-tag color="green">[[ findOutboundTraffic(outbound) ]]</a-tag> <a-tag color="green">[[ findOutboundTraffic(outbound) ]]</a-tag>
</template> </template>
<template slot="test" slot-scope="text, outbound, index">
<a-tooltip>
<template slot="title">{{ i18n "pages.xray.outbound.test"
}}</template>
<a-button
type="primary"
shape="circle"
icon="thunderbolt"
:loading="outboundTestStates[index] && outboundTestStates[index].testing"
@click="testOutbound(index)"
:disabled="(outbound.protocol === 'blackhole' || outbound.tag === 'blocked') || (outboundTestStates[index] && outboundTestStates[index].testing)">
</a-button>
</a-tooltip>
</template>
<template slot="testResult" slot-scope="text, outbound, index">
<div
v-if="outboundTestStates[index] && outboundTestStates[index].result">
<a-tag v-if="outboundTestStates[index].result.success"
color="green">
[[ outboundTestStates[index].result.delay ]]ms
<span v-if="outboundTestStates[index].result.statusCode">
([[ outboundTestStates[index].result.statusCode
]])</span>
</a-tag>
<a-tooltip v-else
:title="outboundTestStates[index].result.error">
<a-tag color="red">
Failed
</a-tag>
</a-tooltip>
</div>
<span
v-else-if="outboundTestStates[index] && outboundTestStates[index].testing">
<a-icon type="loading" />
</span>
<span v-else>-</span>
</template>
</a-table> </a-table>
</a-space> </a-space>
{{end}} {{end}}

View file

@ -1,10 +1,7 @@
{{ template "page/head_start" .}} {{ template "page/head_start" .}}
<link rel="stylesheet" <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/codemirror.min.css?{{ .cur_ver }}">
href="{{ .base_path }}assets/codemirror/codemirror.min.css?{{ .cur_ver }}"> <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
<link rel="stylesheet" <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}">
href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
<link rel="stylesheet"
href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css"> <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css">
{{ template "page/head_end" .}} {{ template "page/head_end" .}}
@ -13,13 +10,10 @@
<a-sidebar></a-sidebar> <a-sidebar></a-sidebar>
<a-layout id="content-layout"> <a-layout id="content-layout">
<a-layout-content> <a-layout-content>
<a-spin :spinning="loadingStates.spinning" :delay="500" <a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
tip='{{ i18n "loading"}}'>
<transition name="list" appear> <transition name="list" appear>
<a-alert type="error" v-if="showAlert && loadingStates.fetched" <a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }"
:style="{ marginBottom: '10px' }" message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
message='{{ i18n "secAlertTitle" }}' color="red"
description='{{ i18n "secAlertSsl" }}' show-icon closable>
</a-alert> </a-alert>
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
@ -32,25 +26,19 @@
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else> <a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
<a-col> <a-col>
<a-card hoverable> <a-card hoverable>
<a-row <a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
:style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
<a-col :xs="24" :sm="10" :style="{ padding: '4px' }"> <a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
<a-space direction="horizontal"> <a-space direction="horizontal">
<a-button type="primary" :disabled="saveBtnDisable" <a-button type="primary" :disabled="saveBtnDisable" @click="updateXraySetting">
@click="updateXraySetting">
{{ i18n "pages.xray.save" }} {{ i18n "pages.xray.save" }}
</a-button> </a-button>
<a-button type="danger" :disabled="!saveBtnDisable" <a-button type="danger" :disabled="!saveBtnDisable" @click="restartXray">
@click="restartXray">
{{ i18n "pages.xray.restart" }} {{ i18n "pages.xray.restart" }}
</a-button> </a-button>
<a-popover v-if="restartResult" <a-popover v-if="restartResult" :overlay-class-name="themeSwitcher.currentTheme">
:overlay-class-name="themeSwitcher.currentTheme"> <span slot="title">{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
<span slot="title">{{ i18n
"pages.index.xrayErrorPopoverTitle" }}</span>
<template slot="content"> <template slot="content">
<span :style="{ maxWidth: '400px' }" <span :style="{ maxWidth: '400px' }" v-for="line in restartResult.split('\n')">[[ line
v-for="line in restartResult.split('\n')">[[ line
]]</span> ]]</span>
</template> </template>
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
@ -60,13 +48,10 @@
<a-col :xs="24" :sm="14"> <a-col :xs="24" :sm="14">
<template> <template>
<div> <div>
<a-back-top <a-back-top :target="() => document.getElementById('content-layout')"
:target="() => document.getElementById('content-layout')"
visibility-height="200"></a-back-top> visibility-height="200"></a-back-top>
<a-alert type="warning" <a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }"
:style="{ float: 'right', width: 'fit-content' }" message='{{ i18n "pages.settings.infoDesc" }}' show-icon>
message='{{ i18n "pages.settings.infoDesc" }}'
show-icon>
</a-alert> </a-alert>
</div> </div>
</template> </template>
@ -75,8 +60,7 @@
</a-card> </a-card>
</a-col> </a-col>
<a-col> <a-col>
<a-tabs default-active-key="tpl-basic" <a-tabs default-active-key="tpl-basic" @change="(activeKey) => { this.changePage(activeKey); }"
@change="(activeKey) => { this.changePage(activeKey); }"
:class="themeSwitcher.currentTheme"> :class="themeSwitcher.currentTheme">
<a-tab-pane key="tpl-basic" :style="{ paddingTop: '20px' }"> <a-tab-pane key="tpl-basic" :style="{ paddingTop: '20px' }">
<template #tab> <template #tab>
@ -99,24 +83,21 @@
</template> </template>
{{ template "settings/xray/outbounds" . }} {{ template "settings/xray/outbounds" . }}
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="tpl-reverse" :style="{ paddingTop: '20px' }" <a-tab-pane key="tpl-reverse" :style="{ paddingTop: '20px' }" force-render="true">
force-render="true">
<template #tab> <template #tab>
<a-icon type="import"></a-icon> <a-icon type="import"></a-icon>
<span>{{ i18n "pages.xray.outbound.reverse"}}</span> <span>{{ i18n "pages.xray.outbound.reverse"}}</span>
</template> </template>
{{ template "settings/xray/reverse" . }} {{ template "settings/xray/reverse" . }}
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="tpl-balancer" :style="{ paddingTop: '20px' }" <a-tab-pane key="tpl-balancer" :style="{ paddingTop: '20px' }" force-render="true">
force-render="true">
<template #tab> <template #tab>
<a-icon type="cluster"></a-icon> <a-icon type="cluster"></a-icon>
<span>{{ i18n "pages.xray.Balancers"}}</span> <span>{{ i18n "pages.xray.Balancers"}}</span>
</template> </template>
{{ template "settings/xray/balancers" . }} {{ template "settings/xray/balancers" . }}
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="tpl-dns" :style="{ paddingTop: '20px' }" <a-tab-pane key="tpl-dns" :style="{ paddingTop: '20px' }" force-render="true">
force-render="true">
<template #tab> <template #tab>
<a-icon type="database"></a-icon> <a-icon type="database"></a-icon>
<span>DNS</span> <span>DNS</span>
@ -139,18 +120,14 @@
</a-layout> </a-layout>
</a-layout> </a-layout>
{{template "page/body_scripts" .}} {{template "page/body_scripts" .}}
<script <script src="{{ .base_path }}assets/js/model/outbound.js?{{ .cur_ver }}"></script>
src="{{ .base_path }}assets/js/model/outbound.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/codemirror/codemirror.min.js?{{ .cur_ver }}"></script>
<script
src="{{ .base_path }}assets/codemirror/codemirror.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/codemirror/javascript.js"></script> <script src="{{ .base_path }}assets/codemirror/javascript.js"></script>
<script src="{{ .base_path }}assets/codemirror/jshint.js"></script> <script src="{{ .base_path }}assets/codemirror/jshint.js"></script>
<script src="{{ .base_path }}assets/codemirror/jsonlint.js"></script> <script src="{{ .base_path }}assets/codemirror/jsonlint.js"></script>
<script src="{{ .base_path }}assets/codemirror/lint/lint.js"></script> <script src="{{ .base_path }}assets/codemirror/lint/lint.js"></script>
<script <script src="{{ .base_path }}assets/codemirror/lint/javascript-lint.js"></script>
src="{{ .base_path }}assets/codemirror/lint/javascript-lint.js"></script> <script src="{{ .base_path }}assets/codemirror/hint/javascript-hint.js"></script>
<script
src="{{ .base_path }}assets/codemirror/hint/javascript-hint.js"></script>
<script src="{{ .base_path }}assets/codemirror/fold/foldcode.js"></script> <script src="{{ .base_path }}assets/codemirror/fold/foldcode.js"></script>
<script src="{{ .base_path }}assets/codemirror/fold/foldgutter.js"></script> <script src="{{ .base_path }}assets/codemirror/fold/foldgutter.js"></script>
<script src="{{ .base_path }}assets/codemirror/fold/brace-fold.js"></script> <script src="{{ .base_path }}assets/codemirror/fold/brace-fold.js"></script>
@ -204,13 +181,11 @@
]; ];
const outboundColumns = [ const outboundColumns = [
{ title: "#", align: 'center', width: 60, scopedSlots: { customRender: 'action' } }, { title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
{ title: '{{ i18n "pages.xray.outbound.tag"}}', dataIndex: 'tag', align: 'center', width: 50 }, { title: '{{ i18n "pages.xray.outbound.tag"}}', dataIndex: 'tag', align: 'center', width: 50 },
{ title: '{{ i18n "protocol"}}', align: 'center', width: 50, scopedSlots: { customRender: 'protocol' } }, { title: '{{ i18n "protocol"}}', align: 'center', width: 50, scopedSlots: { customRender: 'protocol' } },
{ title: '{{ i18n "pages.xray.outbound.address"}}', align: 'center', width: 50, scopedSlots: { customRender: 'address' } }, { title: '{{ i18n "pages.xray.outbound.address"}}', align: 'center', width: 50, scopedSlots: { customRender: 'address' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}', align: 'center', width: 50, scopedSlots: { customRender: 'traffic' } }, { title: '{{ i18n "pages.inbounds.traffic" }}', align: 'center', width: 50, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.xray.outbound.testResult" }}', align: 'center', width: 120, scopedSlots: { customRender: 'testResult' } },
{ title: '{{ i18n "pages.xray.outbound.test" }}', align: 'center', width: 60, scopedSlots: { customRender: 'test' } },
]; ];
const reverseColumns = [ const reverseColumns = [
@ -253,11 +228,8 @@
}, },
oldXraySetting: '', oldXraySetting: '',
xraySetting: '', xraySetting: '',
outboundTestUrl: 'https://www.google.com/generate_204',
oldOutboundTestUrl: 'https://www.google.com/generate_204',
inboundTags: [], inboundTags: [],
outboundsTraffic: [], outboundsTraffic: [],
outboundTestStates: {}, // Track testing state and results for each outbound
saveBtnDisable: true, saveBtnDisable: true,
refreshing: false, refreshing: false,
restartResult: '', restartResult: '',
@ -297,7 +269,7 @@
tag: "direct", tag: "direct",
protocol: "freedom" protocol: "freedom"
}, },
routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"], routingDomainStrategies: ["AsIs", "IpIfNonMatch", "IpOnDemand"],
log: { log: {
loglevel: ["none", "debug", "info", "warning", "error"], loglevel: ["none", "debug", "info", "warning", "error"],
access: ["none", "./access.log"], access: ["none", "./access.log"],
@ -365,14 +337,14 @@
}, },
defaultObservatory: { defaultObservatory: {
subjectSelector: [], subjectSelector: [],
probeURL: "https://www.google.com/generate_204", probeURL: "http://www.google.com/gen_204",
probeInterval: "10m", probeInterval: "10m",
enableConcurrency: true enableConcurrency: true
}, },
defaultBurstObservatory: { defaultBurstObservatory: {
subjectSelector: [], subjectSelector: [],
pingConfig: { pingConfig: {
destination: "https://www.google.com/generate_204", destination: "http://www.google.com/gen_204",
interval: "30m", interval: "30m",
connectivity: "http://connectivitycheck.platform.hicloud.com/generate_204", connectivity: "http://connectivitycheck.platform.hicloud.com/generate_204",
timeout: "10s", timeout: "10s",
@ -403,17 +375,12 @@
this.oldXraySetting = xs; this.oldXraySetting = xs;
this.xraySetting = xs; this.xraySetting = xs;
this.inboundTags = result.inboundTags; this.inboundTags = result.inboundTags;
this.outboundTestUrl = result.outboundTestUrl || 'https://www.google.com/generate_204';
this.oldOutboundTestUrl = this.outboundTestUrl;
this.saveBtnDisable = true; this.saveBtnDisable = true;
} }
}, },
async updateXraySetting() { async updateXraySetting() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post("/panel/xray/update", { const msg = await HttpUtil.post("/panel/xray/update", { xraySetting: this.xraySetting });
xraySetting: this.xraySetting,
outboundTestUrl: this.outboundTestUrl || 'https://www.google.com/generate_204'
});
this.loading(false); this.loading(false);
if (msg.success) { if (msg.success) {
await this.getXraySetting(); await this.getXraySetting();
@ -628,71 +595,6 @@
outbounds.splice(0, 0, outbounds.splice(index, 1)[0]); outbounds.splice(0, 0, outbounds.splice(index, 1)[0]);
this.outboundSettings = JSON.stringify(outbounds); this.outboundSettings = JSON.stringify(outbounds);
}, },
async testOutbound(index) {
const outbound = this.templateSettings.outbounds[index];
if (!outbound) {
Vue.prototype.$message.error('{{ i18n "pages.xray.outbound.testError" }}');
return;
}
if (outbound.protocol === 'blackhole' || outbound.tag === 'blocked') {
Vue.prototype.$message.warning('{{ i18n "pages.xray.outbound.testError" }}: blocked/blackhole outbound');
return;
}
// Initialize test state for this outbound if not exists
if (!this.outboundTestStates[index]) {
this.$set(this.outboundTestStates, index, {
testing: false,
result: null
});
}
// Set testing state
this.$set(this.outboundTestStates[index], 'testing', true);
this.$set(this.outboundTestStates[index], 'result', null);
try {
const outboundJSON = JSON.stringify(outbound);
const allOutboundsJSON = JSON.stringify(this.templateSettings.outbounds || []);
const msg = await HttpUtil.post("/panel/xray/testOutbound", {
outbound: outboundJSON,
allOutbounds: allOutboundsJSON
});
// Update test state
this.$set(this.outboundTestStates[index], 'testing', false);
if (msg.success && msg.obj) {
const result = msg.obj;
this.$set(this.outboundTestStates[index], 'result', result);
if (result.success) {
Vue.prototype.$message.success(
`{{ i18n "pages.xray.outbound.testSuccess" }}: ${result.delay}ms (${result.statusCode})`
);
} else {
Vue.prototype.$message.error(
`{{ i18n "pages.xray.outbound.testFailed" }}: ${result.error || 'Unknown error'}`
);
}
} else {
this.$set(this.outboundTestStates[index], 'result', {
success: false,
error: msg.msg || '{{ i18n "pages.xray.outbound.testError" }}'
});
Vue.prototype.$message.error(msg.msg || '{{ i18n "pages.xray.outbound.testError" }}');
}
} catch (error) {
this.$set(this.outboundTestStates[index], 'testing', false);
this.$set(this.outboundTestStates[index], 'result', {
success: false,
error: error.message || '{{ i18n "pages.xray.outbound.testError" }}'
});
Vue.prototype.$message.error('{{ i18n "pages.xray.outbound.testError" }}: ' + error.message);
}
},
addReverse() { addReverse() {
reverseModal.show({ reverseModal.show({
title: '{{ i18n "pages.xray.outbound.addReverse"}}', title: '{{ i18n "pages.xray.outbound.addReverse"}}',
@ -1066,20 +968,9 @@
await this.getXraySetting(); await this.getXraySetting();
await this.getXrayResult(); await this.getXrayResult();
await this.getOutboundsTraffic(); await this.getOutboundsTraffic();
if (window.wsClient) {
window.wsClient.connect();
window.wsClient.on('outbounds', (payload) => {
if (payload) {
this.outboundsTraffic = payload;
this.$forceUpdate();
}
});
}
while (true) { while (true) {
await PromiseUtil.sleep(800); await PromiseUtil.sleep(800);
this.saveBtnDisable = this.oldXraySetting === this.xraySetting && this.oldOutboundTestUrl === this.outboundTestUrl; this.saveBtnDisable = this.oldXraySetting === this.xraySetting;
} }
}, },
computed: { computed: {

View file

@ -10,7 +10,6 @@ import (
"regexp" "regexp"
"runtime" "runtime"
"sort" "sort"
"strconv"
"time" "time"
"github.com/mhsanaei/3x-ui/v2/database" "github.com/mhsanaei/3x-ui/v2/database"
@ -19,12 +18,6 @@ import (
"github.com/mhsanaei/3x-ui/v2/xray" "github.com/mhsanaei/3x-ui/v2/xray"
) )
// IPWithTimestamp tracks an IP address with its last seen timestamp
type IPWithTimestamp struct {
IP string `json:"ip"`
Timestamp int64 `json:"timestamp"`
}
// CheckClientIpJob monitors client IP addresses from access logs and manages IP blocking based on configured limits. // CheckClientIpJob monitors client IP addresses from access logs and manages IP blocking based on configured limits.
type CheckClientIpJob struct { type CheckClientIpJob struct {
lastClear int64 lastClear int64
@ -126,14 +119,12 @@ func (j *CheckClientIpJob) processLogFile() bool {
ipRegex := regexp.MustCompile(`from (?:tcp:|udp:)?\[?([0-9a-fA-F\.:]+)\]?:\d+ accepted`) ipRegex := regexp.MustCompile(`from (?:tcp:|udp:)?\[?([0-9a-fA-F\.:]+)\]?:\d+ accepted`)
emailRegex := regexp.MustCompile(`email: (.+)$`) emailRegex := regexp.MustCompile(`email: (.+)$`)
timestampRegex := regexp.MustCompile(`^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2})`)
accessLogPath, _ := xray.GetAccessLogPath() accessLogPath, _ := xray.GetAccessLogPath()
file, _ := os.Open(accessLogPath) file, _ := os.Open(accessLogPath)
defer file.Close() defer file.Close()
// Track IPs with their last seen timestamp inboundClientIps := make(map[string]map[string]struct{}, 100)
inboundClientIps := make(map[string]map[string]int64, 100)
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
@ -156,45 +147,28 @@ func (j *CheckClientIpJob) processLogFile() bool {
} }
email := emailMatches[1] email := emailMatches[1]
// Extract timestamp from log line
var timestamp int64
timestampMatches := timestampRegex.FindStringSubmatch(line)
if len(timestampMatches) >= 2 {
t, err := time.Parse("2006/01/02 15:04:05", timestampMatches[1])
if err == nil {
timestamp = t.Unix()
} else {
timestamp = time.Now().Unix()
}
} else {
timestamp = time.Now().Unix()
}
if _, exists := inboundClientIps[email]; !exists { if _, exists := inboundClientIps[email]; !exists {
inboundClientIps[email] = make(map[string]int64) inboundClientIps[email] = make(map[string]struct{})
}
// Update timestamp - keep the latest
if existingTime, ok := inboundClientIps[email][ip]; !ok || timestamp > existingTime {
inboundClientIps[email][ip] = timestamp
} }
inboundClientIps[email][ip] = struct{}{}
} }
shouldCleanLog := false shouldCleanLog := false
for email, ipTimestamps := range inboundClientIps { for email, uniqueIps := range inboundClientIps {
// Convert to IPWithTimestamp slice ips := make([]string, 0, len(uniqueIps))
ipsWithTime := make([]IPWithTimestamp, 0, len(ipTimestamps)) for ip := range uniqueIps {
for ip, timestamp := range ipTimestamps { ips = append(ips, ip)
ipsWithTime = append(ipsWithTime, IPWithTimestamp{IP: ip, Timestamp: timestamp})
} }
sort.Strings(ips)
clientIpsRecord, err := j.getInboundClientIps(email) clientIpsRecord, err := j.getInboundClientIps(email)
if err != nil { if err != nil {
j.addInboundClientIps(email, ipsWithTime) j.addInboundClientIps(email, ips)
continue continue
} }
shouldCleanLog = j.updateInboundClientIps(clientIpsRecord, email, ipsWithTime) || shouldCleanLog shouldCleanLog = j.updateInboundClientIps(clientIpsRecord, email, ips) || shouldCleanLog
} }
return shouldCleanLog return shouldCleanLog
@ -239,9 +213,9 @@ func (j *CheckClientIpJob) getInboundClientIps(clientEmail string) (*model.Inbou
return InboundClientIps, nil return InboundClientIps, nil
} }
func (j *CheckClientIpJob) addInboundClientIps(clientEmail string, ipsWithTime []IPWithTimestamp) error { func (j *CheckClientIpJob) addInboundClientIps(clientEmail string, ips []string) error {
inboundClientIps := &model.InboundClientIps{} inboundClientIps := &model.InboundClientIps{}
jsonIps, err := json.Marshal(ipsWithTime) jsonIps, err := json.Marshal(ips)
j.checkError(err) j.checkError(err)
inboundClientIps.ClientEmail = clientEmail inboundClientIps.ClientEmail = clientEmail
@ -265,8 +239,16 @@ func (j *CheckClientIpJob) addInboundClientIps(clientEmail string, ipsWithTime [
return nil return nil
} }
func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, newIpsWithTime []IPWithTimestamp) bool { func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, ips []string) bool {
// Get the inbound configuration jsonIps, err := json.Marshal(ips)
if err != nil {
logger.Error("failed to marshal IPs to JSON:", err)
return false
}
inboundClientIps.ClientEmail = clientEmail
inboundClientIps.Ips = string(jsonIps)
inbound, err := j.getInboundByEmail(clientEmail) inbound, err := j.getInboundByEmail(clientEmail)
if err != nil { if err != nil {
logger.Errorf("failed to fetch inbound settings for email %s: %s", clientEmail, err) logger.Errorf("failed to fetch inbound settings for email %s: %s", clientEmail, err)
@ -281,57 +263,9 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
settings := map[string][]model.Client{} settings := map[string][]model.Client{}
json.Unmarshal([]byte(inbound.Settings), &settings) json.Unmarshal([]byte(inbound.Settings), &settings)
clients := settings["clients"] clients := settings["clients"]
// Find the client's IP limit
var limitIp int
var clientFound bool
for _, client := range clients {
if client.Email == clientEmail {
limitIp = client.LimitIP
clientFound = true
break
}
}
if !clientFound || limitIp <= 0 || !inbound.Enable {
// No limit or inbound disabled, just update and return
jsonIps, _ := json.Marshal(newIpsWithTime)
inboundClientIps.Ips = string(jsonIps)
db := database.GetDB()
db.Save(inboundClientIps)
return false
}
// Parse old IPs from database
var oldIpsWithTime []IPWithTimestamp
if inboundClientIps.Ips != "" {
json.Unmarshal([]byte(inboundClientIps.Ips), &oldIpsWithTime)
}
// Merge old and new IPs, keeping the latest timestamp for each IP
ipMap := make(map[string]int64)
for _, ipTime := range oldIpsWithTime {
ipMap[ipTime.IP] = ipTime.Timestamp
}
for _, ipTime := range newIpsWithTime {
if existingTime, ok := ipMap[ipTime.IP]; !ok || ipTime.Timestamp > existingTime {
ipMap[ipTime.IP] = ipTime.Timestamp
}
}
// Convert back to slice and sort by timestamp (newest first)
allIps := make([]IPWithTimestamp, 0, len(ipMap))
for ip, timestamp := range ipMap {
allIps = append(allIps, IPWithTimestamp{IP: ip, Timestamp: timestamp})
}
sort.Slice(allIps, func(i, j int) bool {
return allIps[i].Timestamp > allIps[j].Timestamp // Descending order (newest first)
})
shouldCleanLog := false shouldCleanLog := false
j.disAllowedIps = []string{} j.disAllowedIps = []string{}
// Open log file
logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil { if err != nil {
logger.Errorf("failed to open IP limit log file: %s", err) logger.Errorf("failed to open IP limit log file: %s", err)
@ -341,33 +275,27 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
log.SetOutput(logIpFile) log.SetOutput(logIpFile)
log.SetFlags(log.LstdFlags) log.SetFlags(log.LstdFlags)
// Check if we exceed the limit for _, client := range clients {
if len(allIps) > limitIp { if client.Email == clientEmail {
limitIp := client.LimitIP
if limitIp > 0 && inbound.Enable {
shouldCleanLog = true shouldCleanLog = true
// Keep only the newest IPs (up to limitIp) if limitIp < len(ips) {
keptIps := allIps[:limitIp] j.disAllowedIps = append(j.disAllowedIps, ips[limitIp:]...)
disconnectedIps := allIps[limitIp:] for i := limitIp; i < len(ips); i++ {
log.Printf("[LIMIT_IP] Email = %s || SRC = %s", clientEmail, ips[i])
// Log the disconnected IPs (old ones) }
for _, ipTime := range disconnectedIps { }
j.disAllowedIps = append(j.disAllowedIps, ipTime.IP) }
log.Printf("[LIMIT_IP] Email = %s || Disconnecting OLD IP = %s || Timestamp = %d", clientEmail, ipTime.IP, ipTime.Timestamp) }
} }
// Actually disconnect old IPs by temporarily removing and re-adding user sort.Strings(j.disAllowedIps)
// This forces Xray to drop existing connections from old IPs
if len(disconnectedIps) > 0 {
j.disconnectClientTemporarily(inbound, clientEmail, clients)
}
// Update database with only the newest IPs if len(j.disAllowedIps) > 0 {
jsonIps, _ := json.Marshal(keptIps) logger.Debug("disAllowedIps:", j.disAllowedIps)
inboundClientIps.Ips = string(jsonIps)
} else {
// Under limit, save all IPs
jsonIps, _ := json.Marshal(allIps)
inboundClientIps.Ips = string(jsonIps)
} }
db := database.GetDB() db := database.GetDB()
@ -377,68 +305,9 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
return false return false
} }
if len(j.disAllowedIps) > 0 {
logger.Infof("[LIMIT_IP] Client %s: Kept %d newest IPs, disconnected %d old IPs", clientEmail, limitIp, len(j.disAllowedIps))
}
return shouldCleanLog return shouldCleanLog
} }
// disconnectClientTemporarily removes and re-adds a client to force disconnect old connections
func (j *CheckClientIpJob) disconnectClientTemporarily(inbound *model.Inbound, clientEmail string, clients []model.Client) {
var xrayAPI xray.XrayAPI
// Get panel settings for API port
db := database.GetDB()
var apiPort int
var apiPortSetting model.Setting
if err := db.Where("key = ?", "xrayApiPort").First(&apiPortSetting).Error; err == nil {
apiPort, _ = strconv.Atoi(apiPortSetting.Value)
}
if apiPort == 0 {
apiPort = 10085 // Default API port
}
err := xrayAPI.Init(apiPort)
if err != nil {
logger.Warningf("[LIMIT_IP] Failed to init Xray API for disconnection: %v", err)
return
}
defer xrayAPI.Close()
// Find the client config
var clientConfig map[string]any
for _, client := range clients {
if client.Email == clientEmail {
// Convert client to map for API
clientBytes, _ := json.Marshal(client)
json.Unmarshal(clientBytes, &clientConfig)
break
}
}
if clientConfig == nil {
return
}
// Remove user to disconnect all connections
err = xrayAPI.RemoveUser(inbound.Tag, clientEmail)
if err != nil {
logger.Warningf("[LIMIT_IP] Failed to remove user %s: %v", clientEmail, err)
return
}
// Wait a moment for disconnection to take effect
time.Sleep(100 * time.Millisecond)
// Re-add user to allow new connections
err = xrayAPI.AddUser(string(inbound.Protocol), inbound.Tag, clientConfig)
if err != nil {
logger.Warningf("[LIMIT_IP] Failed to re-add user %s: %v", clientEmail, err)
}
}
func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) { func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) {
db := database.GetDB() db := database.GetDB()
inbound := &model.Inbound{} inbound := &model.Inbound{}

View file

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

View file

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

View file

@ -1010,12 +1010,12 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr
if len(traffics) == 0 { if len(traffics) == 0 {
// Empty onlineUsers // Empty onlineUsers
if p != nil { if p != nil {
p.SetOnlineClients(make([]string, 0)) p.SetOnlineClients(nil)
} }
return nil return nil
} }
onlineClients := make([]string, 0) var onlineClients []string
emails := make([]string, 0, len(traffics)) emails := make([]string, 0, len(traffics))
for _, traffic := range traffics { for _, traffic := range traffics {
@ -2141,43 +2141,6 @@ func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error)
if err != nil { if err != nil {
return "", err return "", err
} }
if InboundClientIps.Ips == "" {
return "", nil
}
// Try to parse as new format (with timestamps)
type IPWithTimestamp struct {
IP string `json:"ip"`
Timestamp int64 `json:"timestamp"`
}
var ipsWithTime []IPWithTimestamp
err = json.Unmarshal([]byte(InboundClientIps.Ips), &ipsWithTime)
// If successfully parsed as new format, return with timestamps
if err == nil && len(ipsWithTime) > 0 {
return InboundClientIps.Ips, nil
}
// Otherwise, assume it's old format (simple string array)
// Try to parse as simple array and convert to new format
var oldIps []string
err = json.Unmarshal([]byte(InboundClientIps.Ips), &oldIps)
if err == nil && len(oldIps) > 0 {
// Convert old format to new format with current timestamp
newIpsWithTime := make([]IPWithTimestamp, len(oldIps))
for i, ip := range oldIps {
newIpsWithTime[i] = IPWithTimestamp{
IP: ip,
Timestamp: time.Now().Unix(),
}
}
result, _ := json.Marshal(newIpsWithTime)
return string(result), nil
}
// Return as-is if parsing fails
return InboundClientIps.Ips, nil return InboundClientIps.Ips, nil
} }

View file

@ -1,22 +1,9 @@
package service package service
import ( import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"sync"
"time"
"github.com/mhsanaei/3x-ui/v2/config"
"github.com/mhsanaei/3x-ui/v2/database" "github.com/mhsanaei/3x-ui/v2/database"
"github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/database/model"
"github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/logger"
"github.com/mhsanaei/3x-ui/v2/util/common"
"github.com/mhsanaei/3x-ui/v2/util/json_util"
"github.com/mhsanaei/3x-ui/v2/xray" "github.com/mhsanaei/3x-ui/v2/xray"
"gorm.io/gorm" "gorm.io/gorm"
@ -26,9 +13,6 @@ import (
// It handles outbound traffic monitoring and statistics. // It handles outbound traffic monitoring and statistics.
type OutboundService struct{} type OutboundService struct{}
// testSemaphore limits concurrent outbound tests to prevent resource exhaustion.
var testSemaphore sync.Mutex
func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) { func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
var err error var err error
db := database.GetDB() db := database.GetDB()
@ -116,307 +100,3 @@ func (s *OutboundService) ResetOutboundTraffic(tag string) error {
return nil return nil
} }
// TestOutboundResult represents the result of testing an outbound
type TestOutboundResult struct {
Success bool `json:"success"`
Delay int64 `json:"delay"` // Delay in milliseconds
Error string `json:"error,omitempty"`
StatusCode int `json:"statusCode,omitempty"`
}
// TestOutbound tests an outbound by creating a temporary xray instance and measuring response time.
// allOutboundsJSON must be a JSON array of all outbounds; they are copied into the test config unchanged.
// Only the test inbound and a route rule (to the tested outbound tag) are added.
func (s *OutboundService) TestOutbound(outboundJSON string, testURL string, allOutboundsJSON string) (*TestOutboundResult, error) {
if testURL == "" {
testURL = "https://www.google.com/generate_204"
}
// Limit to one concurrent test at a time
if !testSemaphore.TryLock() {
return &TestOutboundResult{
Success: false,
Error: "Another outbound test is already running, please wait",
}, nil
}
defer testSemaphore.Unlock()
// Parse the outbound being tested to get its tag
var testOutbound map[string]any
if err := json.Unmarshal([]byte(outboundJSON), &testOutbound); err != nil {
return &TestOutboundResult{
Success: false,
Error: fmt.Sprintf("Invalid outbound JSON: %v", err),
}, nil
}
outboundTag, _ := testOutbound["tag"].(string)
if outboundTag == "" {
return &TestOutboundResult{
Success: false,
Error: "Outbound has no tag",
}, nil
}
if protocol, _ := testOutbound["protocol"].(string); protocol == "blackhole" || outboundTag == "blocked" {
return &TestOutboundResult{
Success: false,
Error: "Blocked/blackhole outbound cannot be tested",
}, nil
}
// Use all outbounds when provided; otherwise fall back to single outbound
var allOutbounds []any
if allOutboundsJSON != "" {
if err := json.Unmarshal([]byte(allOutboundsJSON), &allOutbounds); err != nil {
return &TestOutboundResult{
Success: false,
Error: fmt.Sprintf("Invalid allOutbounds JSON: %v", err),
}, nil
}
}
if len(allOutbounds) == 0 {
allOutbounds = []any{testOutbound}
}
// Find an available port for test inbound
testPort, err := findAvailablePort()
if err != nil {
return &TestOutboundResult{
Success: false,
Error: fmt.Sprintf("Failed to find available port: %v", err),
}, nil
}
// Copy all outbounds as-is, add only test inbound and route rule
testConfig := s.createTestConfig(outboundTag, allOutbounds, testPort)
// Use a temporary config file so the main config.json is never overwritten
testConfigPath, err := createTestConfigPath()
if err != nil {
return &TestOutboundResult{
Success: false,
Error: fmt.Sprintf("Failed to create test config path: %v", err),
}, nil
}
defer os.Remove(testConfigPath) // ensure temp file is removed even if process is not stopped
// Create temporary xray process with its own config file
testProcess := xray.NewTestProcess(testConfig, testConfigPath)
defer func() {
if testProcess.IsRunning() {
testProcess.Stop()
}
}()
// Start the test process
if err := testProcess.Start(); err != nil {
return &TestOutboundResult{
Success: false,
Error: fmt.Sprintf("Failed to start test xray instance: %v", err),
}, nil
}
// Wait for xray to start listening on the test port
if err := waitForPort(testPort, 3*time.Second); err != nil {
if !testProcess.IsRunning() {
result := testProcess.GetResult()
return &TestOutboundResult{
Success: false,
Error: fmt.Sprintf("Xray process exited: %s", result),
}, nil
}
return &TestOutboundResult{
Success: false,
Error: fmt.Sprintf("Xray failed to start listening: %v", err),
}, nil
}
// Check if process is still running
if !testProcess.IsRunning() {
result := testProcess.GetResult()
return &TestOutboundResult{
Success: false,
Error: fmt.Sprintf("Xray process exited: %s", result),
}, nil
}
// Test the connection through proxy
delay, statusCode, err := s.testConnection(testPort, testURL)
if err != nil {
return &TestOutboundResult{
Success: false,
Error: err.Error(),
}, nil
}
return &TestOutboundResult{
Success: true,
Delay: delay,
StatusCode: statusCode,
}, nil
}
// createTestConfig creates a test config by copying all outbounds unchanged and adding
// only the test inbound (SOCKS) and a route rule that sends traffic to the given outbound tag.
func (s *OutboundService) createTestConfig(outboundTag string, allOutbounds []any, testPort int) *xray.Config {
// Test inbound (SOCKS proxy) - only addition to inbounds
testInbound := xray.InboundConfig{
Tag: "test-inbound",
Listen: json_util.RawMessage(`"127.0.0.1"`),
Port: testPort,
Protocol: "socks",
Settings: json_util.RawMessage(`{"auth":"noauth","udp":true}`),
}
// Outbounds: copy all, but set noKernelTun=true for WireGuard outbounds
processedOutbounds := make([]any, len(allOutbounds))
for i, ob := range allOutbounds {
outbound, ok := ob.(map[string]any)
if !ok {
processedOutbounds[i] = ob
continue
}
if protocol, ok := outbound["protocol"].(string); ok && protocol == "wireguard" {
// Set noKernelTun to true for WireGuard outbounds
if settings, ok := outbound["settings"].(map[string]any); ok {
settings["noKernelTun"] = true
} else {
// Create settings if it doesn't exist
outbound["settings"] = map[string]any{
"noKernelTun": true,
}
}
}
processedOutbounds[i] = outbound
}
outboundsJSON, _ := json.Marshal(processedOutbounds)
// Create routing rule to route all traffic through test outbound
routingRules := []map[string]any{
{
"type": "field",
"outboundTag": outboundTag,
"network": "tcp,udp",
},
}
routingJSON, _ := json.Marshal(map[string]any{
"domainStrategy": "AsIs",
"rules": routingRules,
})
// Disable logging for test process to avoid creating orphaned log files
logConfig := map[string]any{
"loglevel": "warning",
"access": "none",
"error": "none",
"dnsLog": false,
}
logJSON, _ := json.Marshal(logConfig)
// Create minimal config
cfg := &xray.Config{
LogConfig: json_util.RawMessage(logJSON),
InboundConfigs: []xray.InboundConfig{
testInbound,
},
OutboundConfigs: json_util.RawMessage(string(outboundsJSON)),
RouterConfig: json_util.RawMessage(string(routingJSON)),
Policy: json_util.RawMessage(`{}`),
Stats: json_util.RawMessage(`{}`),
}
return cfg
}
// testConnection tests the connection through the proxy and measures delay.
// It performs a warmup request first to establish the SOCKS connection and populate DNS caches,
// then measures the second request for a more accurate latency reading.
func (s *OutboundService) testConnection(proxyPort int, testURL string) (int64, int, error) {
// Create SOCKS5 proxy URL
proxyURL := fmt.Sprintf("socks5://127.0.0.1:%d", proxyPort)
// Parse proxy URL
proxyURLParsed, err := url.Parse(proxyURL)
if err != nil {
return 0, 0, common.NewErrorf("Invalid proxy URL: %v", err)
}
// Create HTTP client with proxy and keep-alive for connection reuse
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURLParsed),
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 1,
IdleConnTimeout: 10 * time.Second,
DisableCompression: true,
},
}
// Warmup request: establishes SOCKS/TLS connection, DNS, and TCP to the target.
// This mirrors real-world usage where connections are reused.
warmupResp, err := client.Get(testURL)
if err != nil {
return 0, 0, common.NewErrorf("Request failed: %v", err)
}
io.Copy(io.Discard, warmupResp.Body)
warmupResp.Body.Close()
// Measure the actual request on the warm connection
startTime := time.Now()
resp, err := client.Get(testURL)
delay := time.Since(startTime).Milliseconds()
if err != nil {
return 0, 0, common.NewErrorf("Request failed: %v", err)
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
return delay, resp.StatusCode, nil
}
// waitForPort polls until the given TCP port is accepting connections or the timeout expires.
func waitForPort(port int, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 100*time.Millisecond)
if err == nil {
conn.Close()
return nil
}
time.Sleep(50 * time.Millisecond)
}
return fmt.Errorf("port %d not ready after %v", port, timeout)
}
// findAvailablePort finds an available port for testing
func findAvailablePort() (int, error) {
listener, err := net.Listen("tcp", ":0")
if err != nil {
return 0, err
}
defer listener.Close()
addr := listener.Addr().(*net.TCPAddr)
return addr.Port, nil
}
// createTestConfigPath returns a unique path for a temporary xray config file in the bin folder.
// The temp file is created and closed so the path is reserved; Start() will overwrite it.
func createTestConfigPath() (string, error) {
tmpFile, err := os.CreateTemp(config.GetBinFolderPath(), "xray_test_*.json")
if err != nil {
return "", err
}
path := tmpFile.Name()
if err := tmpFile.Close(); err != nil {
os.Remove(path)
return "", err
}
return path, nil
}

View file

@ -567,7 +567,7 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
continue continue
} }
if major > 26 || (major == 26 && minor > 2) || (major == 26 && minor == 2 && patch >= 6) { if major > 25 || (major == 25 && minor > 9) || (major == 25 && minor == 9 && patch >= 11) {
versions = append(versions, release.TagName) versions = append(versions, release.TagName)
} }
} }
@ -1056,79 +1056,44 @@ func (s *ServerService) IsValidGeofileName(filename string) bool {
} }
func (s *ServerService) UpdateGeofile(fileName string) error { func (s *ServerService) UpdateGeofile(fileName string) error {
type geofileEntry struct { files := []struct {
URL string URL string
FileName string FileName string
} }{
geofileAllowlist := map[string]geofileEntry{ {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip.dat"},
"geoip.dat": {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip.dat"}, {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite.dat"},
"geosite.dat": {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite.dat"}, {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat", "geoip_IR.dat"},
"geoip_IR.dat": {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat", "geoip_IR.dat"}, {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat", "geosite_IR.dat"},
"geosite_IR.dat": {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat", "geosite_IR.dat"}, {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip_RU.dat"},
"geoip_RU.dat": {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip_RU.dat"}, {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite_RU.dat"},
"geosite_RU.dat": {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite_RU.dat"},
} }
// Strict allowlist check to avoid writing uncontrolled files // Strict allowlist check to avoid writing uncontrolled files
if fileName != "" { if fileName != "" {
if _, ok := geofileAllowlist[fileName]; !ok { // Use the centralized validation function
return common.NewErrorf("Invalid geofile name: %q not in allowlist", fileName) if !s.IsValidGeofileName(fileName) {
} return common.NewErrorf("Invalid geofile name: contains unsafe path characters: %s", fileName)
} }
// Ensure the filename matches exactly one from our allowlist
isAllowed := false
for _, file := range files {
if fileName == file.FileName {
isAllowed = true
break
}
}
if !isAllowed {
return common.NewErrorf("Invalid geofile name: %s not in allowlist", fileName)
}
}
downloadFile := func(url, destPath string) error { downloadFile := func(url, destPath string) error {
var req *http.Request resp, err := http.Get(url)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return common.NewErrorf("Failed to create HTTP request for %s: %v", url, err)
}
var localFileModTime time.Time
if fileInfo, err := os.Stat(destPath); err == nil {
localFileModTime = fileInfo.ModTime()
if !localFileModTime.IsZero() {
req.Header.Set("If-Modified-Since", localFileModTime.UTC().Format(http.TimeFormat))
}
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil { if err != nil {
return common.NewErrorf("Failed to download Geofile from %s: %v", url, err) return common.NewErrorf("Failed to download Geofile from %s: %v", url, err)
} }
defer resp.Body.Close() defer resp.Body.Close()
// Parse Last-Modified header from server
var serverModTime time.Time
serverModTimeStr := resp.Header.Get("Last-Modified")
if serverModTimeStr != "" {
parsedTime, err := time.Parse(http.TimeFormat, serverModTimeStr)
if err != nil {
logger.Warningf("Failed to parse Last-Modified header for %s: %v", url, err)
} else {
serverModTime = parsedTime
}
}
// Function to update local file's modification time
updateFileModTime := func() {
if !serverModTime.IsZero() {
if err := os.Chtimes(destPath, serverModTime, serverModTime); err != nil {
logger.Warningf("Failed to update modification time for %s: %v", destPath, err)
}
}
}
// Handle 304 Not Modified
if resp.StatusCode == http.StatusNotModified {
updateFileModTime()
return nil
}
if resp.StatusCode != http.StatusOK {
return common.NewErrorf("Failed to download Geofile from %s: received status code %d", url, resp.StatusCode)
}
file, err := os.Create(destPath) file, err := os.Create(destPath)
if err != nil { if err != nil {
return common.NewErrorf("Failed to create Geofile %s: %v", destPath, err) return common.NewErrorf("Failed to create Geofile %s: %v", destPath, err)
@ -1140,25 +1105,39 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
return common.NewErrorf("Failed to save Geofile %s: %v", destPath, err) return common.NewErrorf("Failed to save Geofile %s: %v", destPath, err)
} }
updateFileModTime()
return nil return nil
} }
var errorMessages []string var errorMessages []string
if fileName == "" { if fileName == "" {
// Download all geofiles for _, file := range files {
for _, entry := range geofileAllowlist { // Sanitize the filename from our allowlist as an extra precaution
destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName) destPath := filepath.Join(config.GetBinFolderPath(), filepath.Base(file.FileName))
if err := downloadFile(entry.URL, destPath); err != nil {
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err)) if err := downloadFile(file.URL, destPath); err != nil {
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", file.FileName, err))
} }
} }
} else { } else {
entry := geofileAllowlist[fileName] // Use filepath.Base to ensure we only get the filename component, no path traversal
destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName) safeName := filepath.Base(fileName)
if err := downloadFile(entry.URL, destPath); err != nil { destPath := filepath.Join(config.GetBinFolderPath(), safeName)
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err))
var fileURL string
for _, file := range files {
if file.FileName == fileName {
fileURL = file.URL
break
}
}
if fileURL == "" {
errorMessages = append(errorMessages, fmt.Sprintf("File '%s' not found in the list of Geofiles", fileName))
} else {
if err := downloadFile(fileURL, destPath); err != nil {
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", fileName, err))
}
} }
} }
@ -1226,7 +1205,7 @@ func (s *ServerService) GetNewmldsa65() (any, error) {
return keyPair, nil return keyPair, nil
} }
func (s *ServerService) GetNewEchCert(sni string) (any, error) { func (s *ServerService) GetNewEchCert(sni string) (interface{}, error) {
// Run the command // Run the command
cmd := exec.Command(xray.GetBinaryPath(), "tls", "ech", "--serverName", sni) cmd := exec.Command(xray.GetBinaryPath(), "tls", "ech", "--serverName", sni)
var out bytes.Buffer var out bytes.Buffer
@ -1244,7 +1223,7 @@ func (s *ServerService) GetNewEchCert(sni string) (any, error) {
configList := lines[1] configList := lines[1]
serverKeys := lines[3] serverKeys := lines[3]
return map[string]any{ return map[string]interface{}{
"echServerKeys": serverKeys, "echServerKeys": serverKeys,
"echConfigList": configList, "echConfigList": configList,
}, nil }, nil

View file

@ -5,7 +5,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
@ -54,11 +53,6 @@ var defaultValueMap = map[string]string{
"subEnable": "true", "subEnable": "true",
"subJsonEnable": "false", "subJsonEnable": "false",
"subTitle": "", "subTitle": "",
"subSupportUrl": "",
"subProfileUrl": "",
"subAnnounce": "",
"subEnableRouting": "true",
"subRoutingRules": "",
"subListen": "", "subListen": "",
"subPort": "2096", "subPort": "2096",
"subPath": "/sub/", "subPath": "/sub/",
@ -79,8 +73,6 @@ var defaultValueMap = map[string]string{
"warp": "", "warp": "",
"externalTrafficInformEnable": "false", "externalTrafficInformEnable": "false",
"externalTrafficInformURI": "", "externalTrafficInformURI": "",
"xrayOutboundTestUrl": "https://www.google.com/generate_204",
// LDAP defaults // LDAP defaults
"ldapEnable": "false", "ldapEnable": "false",
"ldapHost": "", "ldapHost": "",
@ -274,14 +266,6 @@ func (s *SettingService) GetXrayConfigTemplate() (string, error) {
return s.getString("xrayTemplateConfig") return s.getString("xrayTemplateConfig")
} }
func (s *SettingService) GetXrayOutboundTestUrl() (string, error) {
return s.getString("xrayOutboundTestUrl")
}
func (s *SettingService) SetXrayOutboundTestUrl(url string) error {
return s.setString("xrayOutboundTestUrl", url)
}
func (s *SettingService) GetListen() (string, error) { func (s *SettingService) GetListen() (string, error) {
return s.getString("webListen") return s.getString("webListen")
} }
@ -475,26 +459,6 @@ func (s *SettingService) GetSubTitle() (string, error) {
return s.getString("subTitle") return s.getString("subTitle")
} }
func (s *SettingService) GetSubSupportUrl() (string, error) {
return s.getString("subSupportUrl")
}
func (s *SettingService) GetSubProfileUrl() (string, error) {
return s.getString("subProfileUrl")
}
func (s *SettingService) GetSubAnnounce() (string, error) {
return s.getString("subAnnounce")
}
func (s *SettingService) GetSubEnableRouting() (bool, error) {
return s.getBool("subEnableRouting")
}
func (s *SettingService) GetSubRoutingRules() (string, error) {
return s.getString("subRoutingRules")
}
func (s *SettingService) GetSubListen() (string, error) { func (s *SettingService) GetSubListen() (string, error) {
return s.getString("subListen") return s.getString("subListen")
} }
@ -718,28 +682,6 @@ func (s *SettingService) GetDefaultXrayConfig() (any, error) {
return jsonData, nil return jsonData, nil
} }
func extractHostname(host string) string {
h, _, err := net.SplitHostPort(host)
// Err is not nil means host does not contain port
if err != nil {
h = host
}
ip := net.ParseIP(h)
// If it's not an IP, return as is
if ip == nil {
return h
}
// If it's an IPv4, return as is
if ip.To4() != nil {
return h
}
// IPv6 needs bracketing
return "[" + h + "]"
}
func (s *SettingService) GetDefaultSettings(host string) (any, error) { func (s *SettingService) GetDefaultSettings(host string) (any, error) {
type settingFunc func() (any, error) type settingFunc func() (any, error)
settings := map[string]settingFunc{ settings := map[string]settingFunc{
@ -790,7 +732,7 @@ func (s *SettingService) GetDefaultSettings(host string) (any, error) {
subTLS = true subTLS = true
} }
if subDomain == "" { if subDomain == "" {
subDomain = extractHostname(host) subDomain = strings.Split(host, ":")[0]
} }
if subTLS { if subTLS {
subURI = "https://" subURI = "https://"

View file

@ -5,7 +5,6 @@ import (
"crypto/rand" "crypto/rand"
"embed" "embed"
"encoding/base64" "encoding/base64"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -2268,8 +2267,6 @@ func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {
} }
// Gather settings to construct absolute URLs // Gather settings to construct absolute URLs
subURI, _ := t.settingService.GetSubURI()
subJsonURI, _ := t.settingService.GetSubJsonURI()
subDomain, _ := t.settingService.GetSubDomain() subDomain, _ := t.settingService.GetSubDomain()
subPort, _ := t.settingService.GetSubPort() subPort, _ := t.settingService.GetSubPort()
subPath, _ := t.settingService.GetSubPath() subPath, _ := t.settingService.GetSubPath()
@ -2317,29 +2314,8 @@ func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {
subJsonPath = subJsonPath + "/" subJsonPath = subJsonPath + "/"
} }
var subURL string subURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subPath, client.SubID)
var subJsonURL string subJsonURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subJsonPath, client.SubID)
// If pre-configured URIs are available, use them directly
if subURI != "" {
if !strings.HasSuffix(subURI, "/") {
subURI = subURI + "/"
}
subURL = fmt.Sprintf("%s%s", subURI, client.SubID)
} else {
subURL = fmt.Sprintf("%s://%s%s%s", scheme, host, subPath, client.SubID)
}
if subJsonURI != "" {
if !strings.HasSuffix(subJsonURI, "/") {
subJsonURI = subJsonURI + "/"
}
subJsonURL = fmt.Sprintf("%s%s", subJsonURI, client.SubID)
} else {
subJsonURL = fmt.Sprintf("%s://%s%s%s", scheme, host, subJsonPath, client.SubID)
}
if !subJsonEnable { if !subJsonEnable {
subJsonURL = "" subJsonURL = ""
} }
@ -3084,41 +3060,9 @@ func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
ips = t.I18nBot("tgbot.noIpRecord") ips = t.I18nBot("tgbot.noIpRecord")
} }
formattedIps := ips
if err == nil && len(ips) > 0 {
type ipWithTimestamp struct {
IP string `json:"ip"`
Timestamp int64 `json:"timestamp"`
}
var ipsWithTime []ipWithTimestamp
if json.Unmarshal([]byte(ips), &ipsWithTime) == nil && len(ipsWithTime) > 0 {
lines := make([]string, 0, len(ipsWithTime))
for _, item := range ipsWithTime {
if item.IP == "" {
continue
}
if item.Timestamp > 0 {
ts := time.Unix(item.Timestamp, 0).Format("2006-01-02 15:04:05")
lines = append(lines, fmt.Sprintf("%s (%s)", item.IP, ts))
continue
}
lines = append(lines, item.IP)
}
if len(lines) > 0 {
formattedIps = strings.Join(lines, "\n")
}
} else {
var oldIps []string
if json.Unmarshal([]byte(ips), &oldIps) == nil && len(oldIps) > 0 {
formattedIps = strings.Join(oldIps, "\n")
}
}
}
output := "" output := ""
output += t.I18nBot("tgbot.messages.email", "Email=="+email) output += t.I18nBot("tgbot.messages.email", "Email=="+email)
output += t.I18nBot("tgbot.messages.ips", "IPs=="+formattedIps) output += t.I18nBot("tgbot.messages.ips", "IPs=="+ips)
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
inlineKeyboard := tu.InlineKeyboard( inlineKeyboard := tu.InlineKeyboard(

View file

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

View file

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

View file

@ -374,16 +374,6 @@
"subJsonEnable" = "Enable/Disable the JSON subscription endpoint independently." "subJsonEnable" = "Enable/Disable the JSON subscription endpoint independently."
"subTitle" = "Subscription Title" "subTitle" = "Subscription Title"
"subTitleDesc" = "Title shown in VPN client" "subTitleDesc" = "Title shown in VPN client"
"subSupportUrl" = "Support URL"
"subSupportUrlDesc" = "Technical support link shown in the VPN client"
"subProfileUrl" = "Profile URL"
"subProfileUrlDesc" = "A link to your website displayed in the VPN client"
"subAnnounce" = "Announce"
"subAnnounceDesc" = "The text of the announce displayed in the VPN client"
"subEnableRouting" = "Enable routing"
"subEnableRoutingDesc" = "Global setting to enable routing in the VPN client. (Only for Happ)"
"subRoutingRules" = "Routing rules"
"subRoutingRulesDesc" = "Global routing rules for the VPN client. (Only for Happ)"
"subListen" = "Listen IP" "subListen" = "Listen IP"
"subListenDesc" = "The IP address for the subscription service. (leave blank to listen on all IPs)" "subListenDesc" = "The IP address for the subscription service. (leave blank to listen on all IPs)"
"subPort" = "Listen Port" "subPort" = "Listen Port"
@ -460,8 +450,6 @@
"FreedomStrategyDesc" = "Set the output strategy for the network in the Freedom Protocol." "FreedomStrategyDesc" = "Set the output strategy for the network in the Freedom Protocol."
"RoutingStrategy" = "Overall Routing Strategy" "RoutingStrategy" = "Overall Routing Strategy"
"RoutingStrategyDesc" = "Set the overall traffic routing strategy for resolving all requests." "RoutingStrategyDesc" = "Set the overall traffic routing strategy for resolving all requests."
"outboundTestUrl" = "Outbound Test URL"
"outboundTestUrlDesc" = "URL used when testing outbound connectivity."
"Torrent" = "Block BitTorrent Protocol" "Torrent" = "Block BitTorrent Protocol"
"Inbounds" = "Inbounds" "Inbounds" = "Inbounds"
"InboundsDesc" = "Accepting the specific clients." "InboundsDesc" = "Accepting the specific clients."
@ -525,12 +513,6 @@
"accountInfo" = "Account Information" "accountInfo" = "Account Information"
"outboundStatus" = "Outbound Status" "outboundStatus" = "Outbound Status"
"sendThrough" = "Send Through" "sendThrough" = "Send Through"
"test" = "Test"
"testResult" = "Test Result"
"testing" = "Testing connection..."
"testSuccess" = "Test successful"
"testFailed" = "Test failed"
"testError" = "Failed to test outbound"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "Add Balancer" "addBalancer" = "Add Balancer"
@ -549,12 +531,6 @@
"psk" = "PreShared Key" "psk" = "PreShared Key"
"domainStrategy" = "Domain Strategy" "domainStrategy" = "Domain Strategy"
[pages.xray.tun]
"nameDesc" = "The name of the TUN interface. Default is 'xray0'"
"mtuDesc" = "Maximum Transmission Unit. The maximum size of data packets. Default is 1500"
"userLevel" = "User Level"
"userLevelDesc" = "All connections made through this inbound will use this user level. Default is 0"
[pages.xray.dns] [pages.xray.dns]
"enable" = "Enable DNS" "enable" = "Enable DNS"
"enableDesc" = "Enable built-in DNS server" "enableDesc" = "Enable built-in DNS server"

View file

@ -9,7 +9,7 @@
"copy" = "Copiar" "copy" = "Copiar"
"copied" = "Copiado" "copied" = "Copiado"
"download" = "Descargar" "download" = "Descargar"
"remark" = "Notas" "remark" = "Nota"
"enable" = "Habilitar" "enable" = "Habilitar"
"protocol" = "Protocolo" "protocol" = "Protocolo"
"search" = "Buscar" "search" = "Buscar"
@ -28,14 +28,14 @@
"edit" = "Editar" "edit" = "Editar"
"delete" = "Eliminar" "delete" = "Eliminar"
"reset" = "Restablecer" "reset" = "Restablecer"
"noData" = "Sin datos" "noData" = "Sin datos."
"copySuccess" = "Copiado exitosamente" "copySuccess" = "Copiado exitosamente"
"sure" = "Seguro" "sure" = "Seguro"
"encryption" = "Encriptación" "encryption" = "Encriptación"
"useIPv4ForHost" = "Usar IPv4 para el host" "useIPv4ForHost" = "Usar IPv4 para el host"
"transmission" = "Transmisión" "transmission" = "Transmisión"
"host" = "Host" "host" = "Anfitrión"
"path" = "Path" "path" = "Ruta"
"camouflage" = "Camuflaje" "camouflage" = "Camuflaje"
"status" = "Estado" "status" = "Estado"
"enabled" = "Habilitado" "enabled" = "Habilitado"
@ -114,7 +114,7 @@
"cpu" = "CPU" "cpu" = "CPU"
"logicalProcessors" = "Procesadores lógicos" "logicalProcessors" = "Procesadores lógicos"
"frequency" = "Frecuencia" "frequency" = "Frecuencia"
"swap" = "Memoria Virtual" "swap" = "Intercambio"
"storage" = "Almacenamiento" "storage" = "Almacenamiento"
"memory" = "RAM" "memory" = "RAM"
"threads" = "Hilos" "threads" = "Hilos"
@ -167,7 +167,7 @@
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "Tráfico Total" "allTimeTraffic" = "Tráfico Total"
"allTimeTrafficUsage" = "Uso de datos histórico" "allTimeTrafficUsage" = "Uso total de todos los tiempos"
"title" = "Entradas" "title" = "Entradas"
"totalDownUp" = "Subidas/Descargas Totales" "totalDownUp" = "Subidas/Descargas Totales"
"totalUsage" = "Uso Total" "totalUsage" = "Uso Total"
@ -283,7 +283,7 @@
"inboundClientAddSuccess" = "Cliente(s) de entrada añadido(s)" "inboundClientAddSuccess" = "Cliente(s) de entrada añadido(s)"
"inboundClientDeleteSuccess" = "Cliente de entrada eliminado" "inboundClientDeleteSuccess" = "Cliente de entrada eliminado"
"inboundClientUpdateSuccess" = "Cliente de entrada actualizado" "inboundClientUpdateSuccess" = "Cliente de entrada actualizado"
"delDepletedClientsSuccess" = "Todos los clientes con tráfico agotado fueron eliminados" "delDepletedClientsSuccess" = "Todos los clientes agotados fueron eliminados"
"resetAllClientTrafficSuccess" = "Todo el tráfico del cliente ha sido reiniciado" "resetAllClientTrafficSuccess" = "Todo el tráfico del cliente ha sido reiniciado"
"resetAllTrafficSuccess" = "Todo el tráfico ha sido reiniciado" "resetAllTrafficSuccess" = "Todo el tráfico ha sido reiniciado"
"resetInboundClientTrafficSuccess" = "El tráfico ha sido reiniciado" "resetInboundClientTrafficSuccess" = "El tráfico ha sido reiniciado"
@ -373,17 +373,7 @@
"subEnableDesc" = "Función de suscripción con configuración separada." "subEnableDesc" = "Función de suscripción con configuración separada."
"subJsonEnable" = "Habilitar/Deshabilitar el endpoint de suscripción JSON de forma independiente." "subJsonEnable" = "Habilitar/Deshabilitar el endpoint de suscripción JSON de forma independiente."
"subTitle" = "Título de la Suscripción" "subTitle" = "Título de la Suscripción"
"subTitleDesc" = "Título mostrado en el cliente VPN" "subTitleDesc" = "Título mostrado en el cliente de VPN"
"subSupportUrl" = "URL de soporte"
"subSupportUrlDesc" = "Enlace de soporte técnico mostrado en el cliente VPN"
"subProfileUrl" = "URL del perfil"
"subProfileUrlDesc" = "Un enlace a tu sitio web mostrado en el cliente VPN"
"subAnnounce" = "Anuncio"
"subAnnounceDesc" = "El texto del anuncio mostrado en el cliente VPN"
"subEnableRouting" = "Habilitar enrutamiento"
"subEnableRoutingDesc" = "Configuración global para habilitar el enrutamiento en el cliente VPN. (Solo para Happ)"
"subRoutingRules" = "Reglas de enrutamiento"
"subRoutingRulesDesc" = "Reglas de enrutamiento globales para el cliente VPN. (Solo para Happ)"
"subListen" = "Listening IP" "subListen" = "Listening IP"
"subListenDesc" = "Dejar en blanco por defecto para monitorear todas las IPs." "subListenDesc" = "Dejar en blanco por defecto para monitorear todas las IPs."
"subPort" = "Puerto de Suscripción" "subPort" = "Puerto de Suscripción"
@ -411,8 +401,8 @@
"fragment" = "Fragmentación" "fragment" = "Fragmentación"
"fragmentDesc" = "Habilitar la fragmentación para el paquete de saludo de TLS" "fragmentDesc" = "Habilitar la fragmentación para el paquete de saludo de TLS"
"fragmentSett" = "Configuración de Fragmentación" "fragmentSett" = "Configuración de Fragmentación"
"noisesDesc" = "Activar Sonidos" "noisesDesc" = "Activar Noises."
"noisesSett" = "Configuración de Sonidos" "noisesSett" = "Configuración de Noises"
"mux" = "Mux" "mux" = "Mux"
"muxDesc" = "Transmite múltiples flujos de datos independientes dentro de un flujo de datos establecido." "muxDesc" = "Transmite múltiples flujos de datos independientes dentro de un flujo de datos establecido."
"muxSett" = "Configuración Mux" "muxSett" = "Configuración Mux"
@ -436,8 +426,8 @@
"stopSuccess" = "Xray se ha detenido correctamente" "stopSuccess" = "Xray se ha detenido correctamente"
"restartError" = "Ocurrió un error al reiniciar Xray." "restartError" = "Ocurrió un error al reiniciar Xray."
"stopError" = "Ocurrió un error al detener Xray." "stopError" = "Ocurrió un error al detener Xray."
"basicTemplate" = "Perfil Básico" "basicTemplate" = "Plantilla Básica"
"advancedTemplate" = "Perfil Avanzado" "advancedTemplate" = "Plantilla Avanzada"
"generalConfigs" = "Configuraciones Generales" "generalConfigs" = "Configuraciones Generales"
"generalConfigsDesc" = "Estas opciones proporcionarán ajustes generales." "generalConfigsDesc" = "Estas opciones proporcionarán ajustes generales."
"logConfigs" = "Registro" "logConfigs" = "Registro"
@ -460,8 +450,6 @@
"FreedomStrategyDesc" = "Establece la estrategia de salida de la red en el Protocolo Freedom." "FreedomStrategyDesc" = "Establece la estrategia de salida de la red en el Protocolo Freedom."
"RoutingStrategy" = "Configurar Estrategia de Enrutamiento de Dominios" "RoutingStrategy" = "Configurar Estrategia de Enrutamiento de Dominios"
"RoutingStrategyDesc" = "Establece la estrategia general de enrutamiento para la resolución de DNS." "RoutingStrategyDesc" = "Establece la estrategia general de enrutamiento para la resolución de DNS."
"outboundTestUrl" = "URL de prueba de outbound"
"outboundTestUrlDesc" = "URL usada al probar la conectividad del outbound"
"Torrent" = "Prohibir Uso de BitTorrent" "Torrent" = "Prohibir Uso de BitTorrent"
"Inbounds" = "Entrante" "Inbounds" = "Entrante"
"InboundsDesc" = "Cambia la plantilla de configuración para aceptar clientes específicos." "InboundsDesc" = "Cambia la plantilla de configuración para aceptar clientes específicos."
@ -543,12 +531,6 @@
"psk" = "Clave precompartida" "psk" = "Clave precompartida"
"domainStrategy" = "Estrategia de dominio" "domainStrategy" = "Estrategia de dominio"
[pages.xray.tun]
"nameDesc" = "El nombre de la interfaz TUN. El valor predeterminado es 'xray0'"
"mtuDesc" = "Unidad Máxima de Transmisión. El tamaño máximo de los paquetes de datos. El valor predeterminado es 1500"
"userLevel" = "Nivel de Usuario"
"userLevelDesc" = "Todas las conexiones realizadas a través de este entrada utilizarán este nivel de usuario. El valor predeterminado es 0"
[pages.xray.dns] [pages.xray.dns]
"enable" = "Habilitar DNS" "enable" = "Habilitar DNS"
"enableDesc" = "Habilitar servidor DNS incorporado" "enableDesc" = "Habilitar servidor DNS incorporado"
@ -612,8 +594,8 @@
[tgbot] [tgbot]
"keyboardClosed" = "❌ Teclado cerrado!" "keyboardClosed" = "❌ Teclado cerrado!"
"noResult" = "❗ ¡Sin resultados!" "noResult" = "❗ ¡No hay resultados!"
"noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor, use el comando nuevamente!" "noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor, use el comando de nuevo!"
"wentWrong" = "❌ ¡Algo salió mal!" "wentWrong" = "❌ ¡Algo salió mal!"
"noIpRecord" = "❗ ¡No hay registro de IP!" "noIpRecord" = "❗ ¡No hay registro de IP!"
"noInbounds" = "❗ ¡No se encontraron entradas!" "noInbounds" = "❗ ¡No se encontraron entradas!"

View file

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

View file

@ -374,16 +374,6 @@
"subJsonEnable" = "Aktifkan/Nonaktifkan endpoint langganan JSON secara mandiri." "subJsonEnable" = "Aktifkan/Nonaktifkan endpoint langganan JSON secara mandiri."
"subTitle" = "Judul Langganan" "subTitle" = "Judul Langganan"
"subTitleDesc" = "Judul yang ditampilkan di klien VPN" "subTitleDesc" = "Judul yang ditampilkan di klien VPN"
"subSupportUrl" = "URL Dukungan"
"subSupportUrlDesc" = "Tautan dukungan teknis yang ditampilkan di klien VPN"
"subProfileUrl" = "URL Profil"
"subProfileUrlDesc" = "Tautan ke situs web Anda yang ditampilkan di klien VPN"
"subAnnounce" = "Pengumuman"
"subAnnounceDesc" = "Teks pengumuman yang ditampilkan di klien VPN"
"subEnableRouting" = "Aktifkan perutean"
"subEnableRoutingDesc" = "Pengaturan global untuk mengaktifkan perutean (routing) di klien VPN. (Hanya untuk Happ)"
"subRoutingRules" = "Aturan routing"
"subRoutingRulesDesc" = "Aturan routing global untuk klien VPN. (Hanya untuk Happ)"
"subListen" = "IP Pendengar" "subListen" = "IP Pendengar"
"subListenDesc" = "Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)" "subListenDesc" = "Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)"
"subPort" = "Port Pendengar" "subPort" = "Port Pendengar"
@ -460,8 +450,6 @@
"FreedomStrategyDesc" = "Atur strategi output untuk jaringan dalam Protokol Freedom." "FreedomStrategyDesc" = "Atur strategi output untuk jaringan dalam Protokol Freedom."
"RoutingStrategy" = "Strategi Pengalihan Keseluruhan" "RoutingStrategy" = "Strategi Pengalihan Keseluruhan"
"RoutingStrategyDesc" = "Atur strategi pengalihan lalu lintas keseluruhan untuk menyelesaikan semua permintaan." "RoutingStrategyDesc" = "Atur strategi pengalihan lalu lintas keseluruhan untuk menyelesaikan semua permintaan."
"outboundTestUrl" = "URL tes outbound"
"outboundTestUrlDesc" = "URL yang digunakan saat menguji konektivitas outbound"
"Torrent" = "Blokir Protokol BitTorrent" "Torrent" = "Blokir Protokol BitTorrent"
"Inbounds" = "Masuk" "Inbounds" = "Masuk"
"InboundsDesc" = "Menerima klien tertentu." "InboundsDesc" = "Menerima klien tertentu."
@ -543,12 +531,6 @@
"psk" = "Kunci Pra-Bagi" "psk" = "Kunci Pra-Bagi"
"domainStrategy" = "Strategi Domain" "domainStrategy" = "Strategi Domain"
[pages.xray.tun]
"nameDesc" = "Nama antarmuka TUN. Standar adalah 'xray0'"
"mtuDesc" = "Unit Transmisi Maksimum. Ukuran maksimum paket data. Standar adalah 1500"
"userLevel" = "Level Pengguna"
"userLevelDesc" = "Semua koneksi yang dibuat melalui inbound ini akan menggunakan level pengguna ini. Standar adalah 0"
[pages.xray.dns] [pages.xray.dns]
"enable" = "Aktifkan DNS" "enable" = "Aktifkan DNS"
"enableDesc" = "Aktifkan server DNS bawaan" "enableDesc" = "Aktifkan server DNS bawaan"

View file

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

View file

@ -374,16 +374,6 @@
"subJsonEnable" = "Ativar/Desativar o endpoint de assinatura JSON de forma independente." "subJsonEnable" = "Ativar/Desativar o endpoint de assinatura JSON de forma independente."
"subTitle" = "Título da Assinatura" "subTitle" = "Título da Assinatura"
"subTitleDesc" = "Título exibido no cliente VPN" "subTitleDesc" = "Título exibido no cliente VPN"
"subSupportUrl" = "URL de Suporte"
"subSupportUrlDesc" = "Link de suporte técnico exibido no cliente VPN"
"subProfileUrl" = "URL de Perfil"
"subProfileUrlDesc" = "Um link para o seu site exibido no cliente VPN"
"subAnnounce" = "Anúncio"
"subAnnounceDesc" = "O texto do anúncio exibido no cliente VPN"
"subEnableRouting" = "Ativar roteamento"
"subEnableRoutingDesc" = "Configuração global para habilitar o roteamento no cliente VPN. (Apenas para Happ)"
"subRoutingRules" = "Regras de roteamento"
"subRoutingRulesDesc" = "Regras de roteamento globais para o cliente VPN. (Apenas para Happ)"
"subListen" = "IP de Escuta" "subListen" = "IP de Escuta"
"subListenDesc" = "O endereço IP para o serviço de assinatura. (deixe em branco para escutar em todos os IPs)" "subListenDesc" = "O endereço IP para o serviço de assinatura. (deixe em branco para escutar em todos os IPs)"
"subPort" = "Porta de Escuta" "subPort" = "Porta de Escuta"
@ -460,8 +450,6 @@
"FreedomStrategyDesc" = "Definir a estratégia de saída para a rede no Protocolo Freedom." "FreedomStrategyDesc" = "Definir a estratégia de saída para a rede no Protocolo Freedom."
"RoutingStrategy" = "Estratégia Geral de Roteamento" "RoutingStrategy" = "Estratégia Geral de Roteamento"
"RoutingStrategyDesc" = "Definir a estratégia geral de roteamento de tráfego para resolver todas as solicitações." "RoutingStrategyDesc" = "Definir a estratégia geral de roteamento de tráfego para resolver todas as solicitações."
"outboundTestUrl" = "URL de teste de outbound"
"outboundTestUrlDesc" = "URL usada ao testar conectividade do outbound"
"Torrent" = "Bloquear Protocolo BitTorrent" "Torrent" = "Bloquear Protocolo BitTorrent"
"Inbounds" = "Inbounds" "Inbounds" = "Inbounds"
"InboundsDesc" = "Aceitar clientes específicos." "InboundsDesc" = "Aceitar clientes específicos."
@ -543,12 +531,6 @@
"psk" = "Chave Pré-Compartilhada" "psk" = "Chave Pré-Compartilhada"
"domainStrategy" = "Estratégia de Domínio" "domainStrategy" = "Estratégia de Domínio"
[pages.xray.tun]
"nameDesc" = "O nome da interface TUN. O padrão é 'xray0'"
"mtuDesc" = "Unidade Máxima de Transmissão. O tamanho máximo dos pacotes de dados. O padrão é 1500"
"userLevel" = "Nível do Usuário"
"userLevelDesc" = "Todas as conexões feitas através deste inbound usarão este nível de usuário. O padrão é 0"
[pages.xray.dns] [pages.xray.dns]
"enable" = "Ativar DNS" "enable" = "Ativar DNS"
"enableDesc" = "Ativar o servidor DNS integrado" "enableDesc" = "Ativar o servidor DNS integrado"

View file

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

View file

@ -374,16 +374,6 @@
"subJsonEnable" = "JSON abonelik uç noktasını bağımsız olarak Etkinleştir/Devre Dışı bırak." "subJsonEnable" = "JSON abonelik uç noktasını bağımsız olarak Etkinleştir/Devre Dışı bırak."
"subTitle" = "Abonelik Başlığı" "subTitle" = "Abonelik Başlığı"
"subTitleDesc" = "VPN istemcisinde gösterilen başlık" "subTitleDesc" = "VPN istemcisinde gösterilen başlık"
"subSupportUrl" = "Destek URL'si"
"subSupportUrlDesc" = "VPN istemcisinde gösterilen teknik destek bağlantısı"
"subProfileUrl" = "Profil URL'si"
"subProfileUrlDesc" = "VPN istemcisinde görüntülenen web sitenize giden bağlantı"
"subAnnounce" = "Duyuru"
"subAnnounceDesc" = "VPN istemcisinde görüntülenen duyuru metni"
"subEnableRouting" = "Yönlendirmeyi etkinleştir"
"subEnableRoutingDesc" = "VPN istemcisinde yönlendirmeyi etkinleştirmek için genel ayar. (Yalnızca Happ için)"
"subRoutingRules" = "Yönlendirme kuralları"
"subRoutingRulesDesc" = "VPN istemcisi için genel yönlendirme kuralları. (Yalnızca Happ için)"
"subListen" = "Dinleme IP" "subListen" = "Dinleme IP"
"subListenDesc" = "Abonelik hizmeti için IP adresi. (tüm IP'leri dinlemek için boş bırakın)" "subListenDesc" = "Abonelik hizmeti için IP adresi. (tüm IP'leri dinlemek için boş bırakın)"
"subPort" = "Dinleme Portu" "subPort" = "Dinleme Portu"
@ -460,8 +450,6 @@
"FreedomStrategyDesc" = "Freedom Protokolünde ağın çıkış stratejisini ayarlayın." "FreedomStrategyDesc" = "Freedom Protokolünde ağın çıkış stratejisini ayarlayın."
"RoutingStrategy" = "Genel Yönlendirme Stratejisi" "RoutingStrategy" = "Genel Yönlendirme Stratejisi"
"RoutingStrategyDesc" = "Tüm istekleri çözmek için genel trafik yönlendirme stratejisini ayarlayın." "RoutingStrategyDesc" = "Tüm istekleri çözmek için genel trafik yönlendirme stratejisini ayarlayın."
"outboundTestUrl" = "Outbound test URL"
"outboundTestUrlDesc" = "Outbound bağlantı testinde kullanılan URL"
"Torrent" = "BitTorrent Protokolünü Engelle" "Torrent" = "BitTorrent Protokolünü Engelle"
"Inbounds" = "Gelenler" "Inbounds" = "Gelenler"
"InboundsDesc" = "Belirli müşterileri kabul eder." "InboundsDesc" = "Belirli müşterileri kabul eder."
@ -543,12 +531,6 @@
"psk" = "Ön Paylaşılan Anahtar" "psk" = "Ön Paylaşılan Anahtar"
"domainStrategy" = "Alan Adı Stratejisi" "domainStrategy" = "Alan Adı Stratejisi"
[pages.xray.tun]
"nameDesc" = "TUN arabiriminin adı. Varsayılan değer 'xray0'dir"
"mtuDesc" = "Maksimum İletim Birimi. Veri paketlerinin maksimum boyutu. Varsayılan değer 1500'dür"
"userLevel" = "Kullanıcı Seviyesi"
"userLevelDesc" = "Bu giriş yoluyla yapılan tüm bağlantılar bu kullanıcı seviyesini kullanacaktır. Varsayılan değer 0'dır"
[pages.xray.dns] [pages.xray.dns]
"enable" = "DNS'yi Etkinleştir" "enable" = "DNS'yi Etkinleştir"
"enableDesc" = "Dahili DNS sunucusunu etkinleştir" "enableDesc" = "Dahili DNS sunucusunu etkinleştir"

View file

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

View file

@ -374,16 +374,6 @@
"subJsonEnable" = "Bật/Tắt điểm cuối đăng ký JSON độc lập." "subJsonEnable" = "Bật/Tắt điểm cuối đăng ký JSON độc lập."
"subTitle" = "Tiêu đề Đăng ký" "subTitle" = "Tiêu đề Đăng ký"
"subTitleDesc" = "Tiêu đề hiển thị trong ứng dụng VPN" "subTitleDesc" = "Tiêu đề hiển thị trong ứng dụng VPN"
"subSupportUrl" = "URL Hỗ trợ"
"subSupportUrlDesc" = "Liên kết hỗ trợ kỹ thuật hiển thị trong ứng dụng VPN"
"subProfileUrl" = "URL Hồ sơ"
"subProfileUrlDesc" = "Liên kết đến trang web của bạn hiển thị trong ứng dụng VPN"
"subAnnounce" = "Thông báo"
"subAnnounceDesc" = "Văn bản thông báo hiển thị trong ứng dụng VPN"
"subEnableRouting" = "Bật định tuyến"
"subEnableRoutingDesc" = "Cài đặt toàn cục để bật định tuyến trong ứng dụng khách VPN. (Chỉ dành cho Happ)"
"subRoutingRules" = "Quy tắc định tuyến"
"subRoutingRulesDesc" = "Quy tắc định tuyến toàn cầu cho client VPN. (Chỉ dành cho Happ)"
"subListen" = "Listening IP" "subListen" = "Listening IP"
"subListenDesc" = "Mặc định để trống để nghe tất cả các IP" "subListenDesc" = "Mặc định để trống để nghe tất cả các IP"
"subPort" = "Cổng gói đăng ký" "subPort" = "Cổng gói đăng ký"
@ -460,8 +450,6 @@
"FreedomStrategyDesc" = "Đặt chiến lược đầu ra của mạng trong Giao thức Freedom." "FreedomStrategyDesc" = "Đặt chiến lược đầu ra của mạng trong Giao thức Freedom."
"RoutingStrategy" = "Cấu hình Chiến lược Định tuyến Tên miền" "RoutingStrategy" = "Cấu hình Chiến lược Định tuyến Tên miền"
"RoutingStrategyDesc" = "Đặt chiến lược định tuyến tổng thể cho việc giải quyết DNS." "RoutingStrategyDesc" = "Đặt chiến lược định tuyến tổng thể cho việc giải quyết DNS."
"outboundTestUrl" = "URL kiểm tra outbound"
"outboundTestUrlDesc" = "URL dùng khi kiểm tra kết nối outbound"
"Torrent" = "Cấu hình sử dụng BitTorrent" "Torrent" = "Cấu hình sử dụng BitTorrent"
"Inbounds" = "Đầu vào" "Inbounds" = "Đầu vào"
"InboundsDesc" = "Thay đổi mẫu cấu hình để chấp nhận các máy khách cụ thể." "InboundsDesc" = "Thay đổi mẫu cấu hình để chấp nhận các máy khách cụ thể."
@ -543,12 +531,6 @@
"psk" = "Khóa chia sẻ" "psk" = "Khóa chia sẻ"
"domainStrategy" = "Chiến lược tên miền" "domainStrategy" = "Chiến lược tên miền"
[pages.xray.tun]
"nameDesc" = "Tên của giao diện TUN. Giá trị mặc định là 'xray0'"
"mtuDesc" = "Đơn vị Truyền Tối đa. Kích thước tối đa của các gói dữ liệu. Giá trị mặc định là 1500"
"userLevel" = "Mức Người Dùng"
"userLevelDesc" = "Tất cả các kết nối được thực hiện thông qua inbound này sẽ sử dụng mức người dùng này. Giá trị mặc định là 0"
[pages.xray.dns] [pages.xray.dns]
"enable" = "Kích hoạt DNS" "enable" = "Kích hoạt DNS"
"enableDesc" = "Kích hoạt máy chủ DNS tích hợp" "enableDesc" = "Kích hoạt máy chủ DNS tích hợp"

View file

@ -374,16 +374,6 @@
"subJsonEnable" = "单独启用/禁用 JSON 订阅端点。" "subJsonEnable" = "单独启用/禁用 JSON 订阅端点。"
"subTitle" = "订阅标题" "subTitle" = "订阅标题"
"subTitleDesc" = "在VPN客户端中显示的标题" "subTitleDesc" = "在VPN客户端中显示的标题"
"subSupportUrl" = "支持链接"
"subSupportUrlDesc" = "VPN 客户端中显示的技术支持链接"
"subProfileUrl" = "个人资料链接"
"subProfileUrlDesc" = "VPN 客户端中显示的网站链接"
"subAnnounce" = "公告"
"subAnnounceDesc" = "VPN 客户端中显示的公告文本"
"subEnableRouting" = "启用路由"
"subEnableRoutingDesc" = "在 VPN 客户端中启用路由的全局设置。(僅限 Happ"
"subRoutingRules" = "路由規則"
"subRoutingRulesDesc" = "VPN 用戶端的全域路由規則。(僅限 Happ"
"subListen" = "监听 IP" "subListen" = "监听 IP"
"subListenDesc" = "订阅服务监听的 IP 地址(留空表示监听所有 IP" "subListenDesc" = "订阅服务监听的 IP 地址(留空表示监听所有 IP"
"subPort" = "监听端口" "subPort" = "监听端口"
@ -460,8 +450,6 @@
"FreedomStrategyDesc" = "设置 Freedom 协议中网络的输出策略" "FreedomStrategyDesc" = "设置 Freedom 协议中网络的输出策略"
"RoutingStrategy" = "配置路由域策略" "RoutingStrategy" = "配置路由域策略"
"RoutingStrategyDesc" = "设置 DNS 解析的整体路由策略" "RoutingStrategyDesc" = "设置 DNS 解析的整体路由策略"
"outboundTestUrl" = "出站测试 URL"
"outboundTestUrlDesc" = "测试出站连接时使用的 URL"
"Torrent" = "屏蔽 BitTorrent 协议" "Torrent" = "屏蔽 BitTorrent 协议"
"Inbounds" = "入站规则" "Inbounds" = "入站规则"
"InboundsDesc" = "接受来自特定客户端的流量" "InboundsDesc" = "接受来自特定客户端的流量"
@ -543,12 +531,6 @@
"psk" = "共享密钥" "psk" = "共享密钥"
"domainStrategy" = "域策略" "domainStrategy" = "域策略"
[pages.xray.tun]
"nameDesc" = "TUN 接口的名称。默认值为 'xray0'"
"mtuDesc" = "最大传输单元。数据包的最大大小。默认值为 1500"
"userLevel" = "用户级别"
"userLevelDesc" = "通过此入站的所有连接都将使用此用户级别。默认值为 0"
[pages.xray.dns] [pages.xray.dns]
"enable" = "启用 DNS" "enable" = "启用 DNS"
"enableDesc" = "启用内置 DNS 服务器" "enableDesc" = "启用内置 DNS 服务器"

View file

@ -374,16 +374,6 @@
"subJsonEnable" = "獨立啟用/停用 JSON 訂閱端點。" "subJsonEnable" = "獨立啟用/停用 JSON 訂閱端點。"
"subTitle" = "訂閱標題" "subTitle" = "訂閱標題"
"subTitleDesc" = "在VPN客戶端中顯示的標題" "subTitleDesc" = "在VPN客戶端中顯示的標題"
"subSupportUrl" = "支援連結"
"subSupportUrlDesc" = "VPN 用戶端中顯示的技術支援連結"
"subProfileUrl" = "個人資料連結"
"subProfileUrlDesc" = "VPN 用戶端中顯示的網站連結"
"subAnnounce" = "公告"
"subAnnounceDesc" = "VPN 用戶端中顯示的公告文字"
"subEnableRouting" = "啟用路由"
"subEnableRoutingDesc" = "在 VPN 用戶端中啟用路由的全域設定。(僅限 Happ"
"subRoutingRules" = "路由規則"
"subRoutingRulesDesc" = "VPN 用戶端的全域路由規則。(僅限 Happ"
"subListen" = "監聽 IP" "subListen" = "監聽 IP"
"subListenDesc" = "訂閱服務監聽的 IP 地址(留空表示監聽所有 IP" "subListenDesc" = "訂閱服務監聽的 IP 地址(留空表示監聽所有 IP"
"subPort" = "監聽埠" "subPort" = "監聽埠"
@ -460,8 +450,6 @@
"FreedomStrategyDesc" = "設定 Freedom 協議中網路的輸出策略" "FreedomStrategyDesc" = "設定 Freedom 協議中網路的輸出策略"
"RoutingStrategy" = "配置路由域策略" "RoutingStrategy" = "配置路由域策略"
"RoutingStrategyDesc" = "設定 DNS 解析的整體路由策略" "RoutingStrategyDesc" = "設定 DNS 解析的整體路由策略"
"outboundTestUrl" = "出站測試 URL"
"outboundTestUrlDesc" = "測試出站連線時使用的 URL"
"Torrent" = "遮蔽 BitTorrent 協議" "Torrent" = "遮蔽 BitTorrent 協議"
"Inbounds" = "入站規則" "Inbounds" = "入站規則"
"InboundsDesc" = "接受來自特定客戶端的流量" "InboundsDesc" = "接受來自特定客戶端的流量"
@ -543,12 +531,6 @@
"psk" = "共享金鑰" "psk" = "共享金鑰"
"domainStrategy" = "域策略" "domainStrategy" = "域策略"
[pages.xray.tun]
"nameDesc" = "TUN 介面的名稱。預設值為 'xray0'"
"mtuDesc" = "最大傳輸單元。資料包的最大大小。預設值為 1500"
"userLevel" = "用戶級別"
"userLevelDesc" = "通過此入站的所有連接都將使用此用戶級別。預設值為 0"
[pages.xray.dns] [pages.xray.dns]
"enable" = "啟用 DNS" "enable" = "啟用 DNS"
"enableDesc" = "啟用內建 DNS 伺服器" "enableDesc" = "啟用內建 DNS 伺服器"

View file

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

View file

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

View file

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

Binary file not shown.

View file

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

349
x-ui.sh
View file

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

View file

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

View file

@ -110,15 +110,6 @@ func NewProcess(xrayConfig *Config) *Process {
return p return p
} }
// NewTestProcess creates a new Xray process that uses a specific config file path.
// Used for test runs (e.g. outbound test) so the main config.json is not overwritten.
// The config file at configPath is removed when the process is stopped.
func NewTestProcess(xrayConfig *Config, configPath string) *Process {
p := &Process{newTestProcess(xrayConfig, configPath)}
runtime.SetFinalizer(p, stopProcess)
return p
}
type process struct { type process struct {
cmd *exec.Cmd cmd *exec.Cmd
@ -128,7 +119,6 @@ type process struct {
onlineClients []string onlineClients []string
config *Config config *Config
configPath string // if set, use this path instead of GetConfigPath() and remove on Stop
logWriter *LogWriter logWriter *LogWriter
exitErr error exitErr error
startTime time.Time startTime time.Time
@ -144,13 +134,6 @@ func newProcess(config *Config) *process {
} }
} }
// newTestProcess creates a process that writes and runs with a specific config path.
func newTestProcess(config *Config, configPath string) *process {
p := newProcess(config)
p.configPath = configPath
return p
}
// IsRunning returns true if the Xray process is currently running. // IsRunning returns true if the Xray process is currently running.
func (p *process) IsRunning() bool { func (p *process) IsRunning() bool {
if p.cmd == nil || p.cmd.Process == nil { if p.cmd == nil || p.cmd.Process == nil {
@ -255,9 +238,6 @@ func (p *process) Start() (err error) {
} }
configPath := GetConfigPath() configPath := GetConfigPath()
if p.configPath != "" {
configPath = p.configPath
}
err = os.WriteFile(configPath, data, fs.ModePerm) err = os.WriteFile(configPath, data, fs.ModePerm)
if err != nil { if err != nil {
return common.NewErrorf("Failed to write configuration file: %v", err) return common.NewErrorf("Failed to write configuration file: %v", err)
@ -298,16 +278,6 @@ func (p *process) Stop() error {
return errors.New("xray is not running") return errors.New("xray is not running")
} }
// Remove temporary config file used for test runs so main config is never touched
if p.configPath != "" {
if p.configPath != GetConfigPath() {
// Check if file exists before removing
if _, err := os.Stat(p.configPath); err == nil {
_ = os.Remove(p.configPath)
}
}
}
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
return p.cmd.Process.Kill() return p.cmd.Process.Kill()
} else { } else {