mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-02-13 22:07:59 +00:00
Compare commits
No commits in common. "main" and "v2.8.7" have entirely different histories.
64 changed files with 1243 additions and 4168 deletions
155
.github/copilot-instructions.md
vendored
155
.github/copilot-instructions.md
vendored
|
|
@ -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
|
|
||||||
31
.github/workflows/cleanup_caches.yml
vendored
31
.github/workflows/cleanup_caches.yml
vendored
|
|
@ -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
|
|
||||||
41
.github/workflows/release.yml
vendored
41
.github/workflows/release.yml
vendored
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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}"
|
||||||
|
|
|
||||||
|
|
@ -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/
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
2.8.10
|
2.8.7
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
56
go.mod
56
go.mod
|
|
@ -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.69.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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
111
go.sum
111
go.sum
|
|
@ -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,6 +187,8 @@ 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.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||||
|
|
@ -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=
|
||||||
|
|
|
||||||
169
install.sh
169
install.sh
|
|
@ -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 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 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 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 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 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 socat
|
||||||
;;
|
;;
|
||||||
alpine)
|
alpine)
|
||||||
apk update && apk add curl tar tzdata socat ca-certificates
|
apk update && apk add curl tar tzdata 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 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
|
||||||
|
|
@ -197,7 +180,7 @@ setup_ip_certificate() {
|
||||||
|
|
||||||
echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}"
|
echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}"
|
||||||
echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}"
|
echo -e "${yellow}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}"
|
echo -e "${yellow}Port 80 must be open and accessible from the internet.${plain}"
|
||||||
|
|
||||||
# Check for acme.sh
|
# Check for acme.sh
|
||||||
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||||
|
|
@ -233,46 +216,9 @@ setup_ip_certificate() {
|
||||||
# Set reload command for auto-renewal (add || true so it doesn't fail during first install)
|
# 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"
|
local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null || true"
|
||||||
|
|
||||||
# Choose port for HTTP-01 listener (default 80, prompt override)
|
|
||||||
local WebPort=""
|
|
||||||
read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort
|
|
||||||
WebPort="${WebPort:-80}"
|
|
||||||
if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then
|
|
||||||
echo -e "${red}Invalid port provided. Falling back to 80.${plain}"
|
|
||||||
WebPort=80
|
|
||||||
fi
|
|
||||||
echo -e "${green}Using port ${WebPort} for standalone validation.${plain}"
|
|
||||||
if [[ "${WebPort}" -ne 80 ]]; then
|
|
||||||
echo -e "${yellow}Reminder: Let's Encrypt still connects on port 80; forward external port 80 to ${WebPort}.${plain}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure chosen port is available
|
|
||||||
while true; do
|
|
||||||
if is_port_in_use "${WebPort}"; then
|
|
||||||
echo -e "${yellow}Port ${WebPort} is in use.${plain}"
|
|
||||||
|
|
||||||
local alt_port=""
|
|
||||||
read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port
|
|
||||||
alt_port="${alt_port// /}"
|
|
||||||
if [[ -z "${alt_port}" ]]; then
|
|
||||||
echo -e "${red}Port ${WebPort} is busy; cannot proceed.${plain}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then
|
|
||||||
echo -e "${red}Invalid port provided.${plain}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
WebPort="${alt_port}"
|
|
||||||
continue
|
|
||||||
else
|
|
||||||
echo -e "${green}Port ${WebPort} is free and ready for standalone validation.${plain}"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Issue certificate with shortlived profile
|
# Issue certificate with shortlived profile
|
||||||
echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}"
|
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 --set-default-ca --server letsencrypt >/dev/null 2>&1
|
||||||
|
|
||||||
~/.acme.sh/acme.sh --issue \
|
~/.acme.sh/acme.sh --issue \
|
||||||
${domain_args} \
|
${domain_args} \
|
||||||
|
|
@ -280,12 +226,12 @@ setup_ip_certificate() {
|
||||||
--server letsencrypt \
|
--server letsencrypt \
|
||||||
--certificate-profile shortlived \
|
--certificate-profile shortlived \
|
||||||
--days 6 \
|
--days 6 \
|
||||||
--httpport ${WebPort} \
|
--httpport 80 \
|
||||||
--force
|
--force
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo -e "${red}Failed to issue IP certificate${plain}"
|
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}"
|
echo -e "${yellow}Please ensure port 80 is open and accessible from the internet${plain}"
|
||||||
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
|
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
|
||||||
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
|
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
|
||||||
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
|
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
|
||||||
|
|
@ -414,7 +360,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}"
|
||||||
|
|
@ -521,13 +467,12 @@ prompt_and_setup_ssl() {
|
||||||
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 for Domain (90-day validity, auto-renews)"
|
||||||
echo -e "${green}2.${plain} Let's Encrypt for IP Address (6-day validity, auto-renews)"
|
echo -e "${green}2.${plain} Let's Encrypt for IP Address (6-day validity, auto-renews)"
|
||||||
echo -e "${green}3.${plain} Custom SSL Certificate (Path to existing files)"
|
echo -e "${blue}Note:${plain} Both options require port 80 open. IP certs use shortlived profile."
|
||||||
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
|
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 (IP cert) if not 1
|
||||||
if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" ]]; then
|
if [[ "$ssl_choice" != "1" ]]; then
|
||||||
ssl_choice="2"
|
ssl_choice="2"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -570,66 +515,7 @@ prompt_and_setup_ssl() {
|
||||||
echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}"
|
echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}"
|
||||||
SSL_HOST="${server_ip}"
|
SSL_HOST="${server_ip}"
|
||||||
fi
|
fi
|
||||||
;;
|
|
||||||
3)
|
|
||||||
# User chose Custom Paths (User Provided) option
|
|
||||||
echo -e "${green}Using custom existing certificate...${plain}"
|
|
||||||
local custom_cert=""
|
|
||||||
local custom_key=""
|
|
||||||
local custom_domain=""
|
|
||||||
|
|
||||||
# 3.1 Request Domain to compose Panel URL later
|
|
||||||
read -rp "Please enter domain name certificate issued for: " custom_domain
|
|
||||||
custom_domain="${custom_domain// /}" # Убираем пробелы
|
|
||||||
|
|
||||||
# 3.2 Loop for Certificate Path
|
|
||||||
while true; do
|
|
||||||
read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert
|
|
||||||
# Strip quotes if present
|
|
||||||
custom_cert=$(echo "$custom_cert" | tr -d '"' | tr -d "'")
|
|
||||||
|
|
||||||
if [[ -f "$custom_cert" && -r "$custom_cert" && -s "$custom_cert" ]]; then
|
|
||||||
break
|
|
||||||
elif [[ ! -f "$custom_cert" ]]; then
|
|
||||||
echo -e "${red}Error: File does not exist! Try again.${plain}"
|
|
||||||
elif [[ ! -r "$custom_cert" ]]; then
|
|
||||||
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
|
|
||||||
else
|
|
||||||
echo -e "${red}Error: File is empty!${plain}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# 3.3 Loop for Private Key Path
|
|
||||||
while true; do
|
|
||||||
read -rp "Input private key path (keywords: .key / privatekey): " custom_key
|
|
||||||
# Strip quotes if present
|
|
||||||
custom_key=$(echo "$custom_key" | tr -d '"' | tr -d "'")
|
|
||||||
|
|
||||||
if [[ -f "$custom_key" && -r "$custom_key" && -s "$custom_key" ]]; then
|
|
||||||
break
|
|
||||||
elif [[ ! -f "$custom_key" ]]; then
|
|
||||||
echo -e "${red}Error: File does not exist! Try again.${plain}"
|
|
||||||
elif [[ ! -r "$custom_key" ]]; then
|
|
||||||
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
|
|
||||||
else
|
|
||||||
echo -e "${red}Error: File is empty!${plain}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# 3.4 Apply Settings via x-ui binary
|
|
||||||
${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" >/dev/null 2>&1
|
|
||||||
|
|
||||||
# Set SSL_HOST for composing Panel URL
|
|
||||||
if [[ -n "$custom_domain" ]]; then
|
|
||||||
SSL_HOST="$custom_domain"
|
|
||||||
else
|
|
||||||
SSL_HOST="${server_ip}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${green}✓ Custom certificate paths applied.${plain}"
|
|
||||||
echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}"
|
|
||||||
|
|
||||||
systemctl restart x-ui >/dev/null 2>&1 || rc-service x-ui restart >/dev/null 2>&1
|
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
|
echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
|
||||||
|
|
@ -654,11 +540,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
|
||||||
|
|
@ -881,15 +764,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 +783,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
|
||||||
;;
|
;;
|
||||||
|
|
|
||||||
28
sub/sub.go
28
sub/sub.go
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -624,6 +632,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 +698,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 +837,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 +870,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
156
update.sh
156
update.sh
|
|
@ -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() {
|
||||||
|
|
@ -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
|
||||||
|
|
@ -222,7 +205,7 @@ setup_ip_certificate() {
|
||||||
|
|
||||||
echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}"
|
echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}"
|
||||||
echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}"
|
echo -e "${yellow}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}"
|
echo -e "${yellow}Port 80 must be open and accessible from the internet.${plain}"
|
||||||
|
|
||||||
# Check for acme.sh
|
# Check for acme.sh
|
||||||
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||||
|
|
@ -258,46 +241,9 @@ setup_ip_certificate() {
|
||||||
# Set reload command for auto-renewal (add || true so it doesn't fail if service stopped)
|
# 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"
|
local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null || true"
|
||||||
|
|
||||||
# Choose port for HTTP-01 listener (default 80, prompt override)
|
|
||||||
local WebPort=""
|
|
||||||
read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort
|
|
||||||
WebPort="${WebPort:-80}"
|
|
||||||
if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then
|
|
||||||
echo -e "${red}Invalid port provided. Falling back to 80.${plain}"
|
|
||||||
WebPort=80
|
|
||||||
fi
|
|
||||||
echo -e "${green}Using port ${WebPort} for standalone validation.${plain}"
|
|
||||||
if [[ "${WebPort}" -ne 80 ]]; then
|
|
||||||
echo -e "${yellow}Reminder: Let's Encrypt still connects on port 80; forward external port 80 to ${WebPort}.${plain}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure chosen port is available
|
|
||||||
while true; do
|
|
||||||
if is_port_in_use "${WebPort}"; then
|
|
||||||
echo -e "${yellow}Port ${WebPort} is currently in use.${plain}"
|
|
||||||
|
|
||||||
local alt_port=""
|
|
||||||
read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port
|
|
||||||
alt_port="${alt_port// /}"
|
|
||||||
if [[ -z "${alt_port}" ]]; then
|
|
||||||
echo -e "${red}Port ${WebPort} is busy; cannot proceed.${plain}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then
|
|
||||||
echo -e "${red}Invalid port provided.${plain}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
WebPort="${alt_port}"
|
|
||||||
continue
|
|
||||||
else
|
|
||||||
echo -e "${green}Port ${WebPort} is free and ready for standalone validation.${plain}"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Issue certificate with shortlived profile
|
# Issue certificate with shortlived profile
|
||||||
echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}"
|
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 --set-default-ca --server letsencrypt >/dev/null 2>&1
|
||||||
|
|
||||||
~/.acme.sh/acme.sh --issue \
|
~/.acme.sh/acme.sh --issue \
|
||||||
${domain_args} \
|
${domain_args} \
|
||||||
|
|
@ -305,12 +251,12 @@ setup_ip_certificate() {
|
||||||
--server letsencrypt \
|
--server letsencrypt \
|
||||||
--certificate-profile shortlived \
|
--certificate-profile shortlived \
|
||||||
--days 6 \
|
--days 6 \
|
||||||
--httpport ${WebPort} \
|
--httpport 80 \
|
||||||
--force
|
--force
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo -e "${red}Failed to issue IP certificate${plain}"
|
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}"
|
echo -e "${yellow}Please ensure port 80 is open and accessible from the internet${plain}"
|
||||||
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
|
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
|
||||||
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
|
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
|
||||||
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
|
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
|
||||||
|
|
@ -437,7 +383,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}"
|
||||||
|
|
@ -541,13 +487,12 @@ prompt_and_setup_ssl() {
|
||||||
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 for Domain (90-day validity, auto-renews)"
|
||||||
echo -e "${green}2.${plain} Let's Encrypt for IP Address (6-day validity, auto-renews)"
|
echo -e "${green}2.${plain} Let's Encrypt for IP Address (6-day validity, auto-renews)"
|
||||||
echo -e "${green}3.${plain} Custom SSL Certificate (Path to existing files)"
|
echo -e "${blue}Note:${plain} Both options require port 80 open. IP certs use shortlived profile."
|
||||||
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
|
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 (IP cert) if not 1
|
||||||
if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" ]]; then
|
if [[ "$ssl_choice" != "1" ]]; then
|
||||||
ssl_choice="2"
|
ssl_choice="2"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -597,67 +542,6 @@ prompt_and_setup_ssl() {
|
||||||
else
|
else
|
||||||
systemctl restart x-ui >/dev/null 2>&1
|
systemctl restart x-ui >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
;;
|
|
||||||
3)
|
|
||||||
# User chose Custom Paths (User Provided) option
|
|
||||||
echo -e "${green}Using custom existing certificate...${plain}"
|
|
||||||
local custom_cert=""
|
|
||||||
local custom_key=""
|
|
||||||
local custom_domain=""
|
|
||||||
|
|
||||||
# 3.1 Request Domain to compose Panel URL later
|
|
||||||
read -rp "Please enter domain name certificate issued for: " custom_domain
|
|
||||||
custom_domain="${custom_domain// /}" # Убираем пробелы
|
|
||||||
|
|
||||||
# 3.2 Loop for Certificate Path
|
|
||||||
while true; do
|
|
||||||
read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert
|
|
||||||
# Strip quotes if present
|
|
||||||
custom_cert=$(echo "$custom_cert" | tr -d '"' | tr -d "'")
|
|
||||||
|
|
||||||
if [[ -f "$custom_cert" && -r "$custom_cert" && -s "$custom_cert" ]]; then
|
|
||||||
break
|
|
||||||
elif [[ ! -f "$custom_cert" ]]; then
|
|
||||||
echo -e "${red}Error: File does not exist! Try again.${plain}"
|
|
||||||
elif [[ ! -r "$custom_cert" ]]; then
|
|
||||||
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
|
|
||||||
else
|
|
||||||
echo -e "${red}Error: File is empty!${plain}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# 3.3 Loop for Private Key Path
|
|
||||||
while true; do
|
|
||||||
read -rp "Input private key path (keywords: .key / privatekey): " custom_key
|
|
||||||
# Strip quotes if present
|
|
||||||
custom_key=$(echo "$custom_key" | tr -d '"' | tr -d "'")
|
|
||||||
|
|
||||||
if [[ -f "$custom_key" && -r "$custom_key" && -s "$custom_key" ]]; then
|
|
||||||
break
|
|
||||||
elif [[ ! -f "$custom_key" ]]; then
|
|
||||||
echo -e "${red}Error: File does not exist! Try again.${plain}"
|
|
||||||
elif [[ ! -r "$custom_key" ]]; then
|
|
||||||
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
|
|
||||||
else
|
|
||||||
echo -e "${red}Error: File is empty!${plain}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# 3.4 Apply Settings via x-ui binary
|
|
||||||
${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" >/dev/null 2>&1
|
|
||||||
|
|
||||||
# Set SSL_HOST for composing Panel URL
|
|
||||||
if [[ -n "$custom_domain" ]]; then
|
|
||||||
SSL_HOST="$custom_domain"
|
|
||||||
else
|
|
||||||
SSL_HOST="${server_ip}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${green}✓ Custom certificate paths applied.${plain}"
|
|
||||||
echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}"
|
|
||||||
|
|
||||||
systemctl restart x-ui >/dev/null 2>&1 || rc-service x-ui restart >/dev/null 2>&1
|
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
|
echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
|
||||||
|
|
@ -687,11 +571,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
|
||||||
|
|
@ -802,7 +683,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 +765,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 +783,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
|
||||||
;;
|
;;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ const Protocols = {
|
||||||
MIXED: 'mixed',
|
MIXED: 'mixed',
|
||||||
HTTP: 'http',
|
HTTP: 'http',
|
||||||
WIREGUARD: 'wireguard',
|
WIREGUARD: 'wireguard',
|
||||||
TUN: 'tun',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SSMethods = {
|
const SSMethods = {
|
||||||
|
|
@ -318,13 +317,15 @@ TcpStreamSettings.TcpResponse = class extends XrayCommonClass {
|
||||||
|
|
||||||
class KcpStreamSettings extends XrayCommonClass {
|
class KcpStreamSettings extends XrayCommonClass {
|
||||||
constructor(
|
constructor(
|
||||||
mtu = 1350,
|
mtu = 1250,
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2597,7 +2506,7 @@ Inbound.HttpSettings.HttpAccount = class extends XrayCommonClass {
|
||||||
Inbound.WireguardSettings = class extends XrayCommonClass {
|
Inbound.WireguardSettings = class extends XrayCommonClass {
|
||||||
constructor(
|
constructor(
|
||||||
protocol,
|
protocol,
|
||||||
mtu = 1420,
|
mtu = 1250,
|
||||||
secretKey = Wireguard.generateKeypair().privateKey,
|
secretKey = Wireguard.generateKeypair().privateKey,
|
||||||
peers = [new Inbound.WireguardSettings.Peer()],
|
peers = [new Inbound.WireguardSettings.Peer()],
|
||||||
noKernelTun = false
|
noKernelTun = false
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
@ -165,13 +164,15 @@ class TcpStreamSettings extends CommonClass {
|
||||||
|
|
||||||
class KcpStreamSettings extends CommonClass {
|
class KcpStreamSettings extends CommonClass {
|
||||||
constructor(
|
constructor(
|
||||||
mtu = 1350,
|
mtu = 1250,
|
||||||
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¶m2=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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -1478,7 +1233,7 @@ Outbound.HttpSettings = class extends CommonClass {
|
||||||
|
|
||||||
Outbound.WireguardSettings = class extends CommonClass {
|
Outbound.WireguardSettings = class extends CommonClass {
|
||||||
constructor(
|
constructor(
|
||||||
mtu = 1420,
|
mtu = 1250,
|
||||||
secretKey = '',
|
secretKey = '',
|
||||||
address = [''],
|
address = [''],
|
||||||
workers = 2,
|
workers = 2,
|
||||||
|
|
@ -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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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/";
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"}}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}}
|
|
||||||
|
|
@ -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}}
|
|
||||||
|
|
@ -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}}
|
||||||
|
|
|
||||||
|
|
@ -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}}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -1602,6 +1602,7 @@
|
||||||
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1613,19 +1614,7 @@
|
||||||
|
|
||||||
// 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 +1622,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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -554,78 +478,17 @@
|
||||||
</table>
|
</table>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
<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,11 +626,10 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.infoModal.clientIps = 'No IP Record';
|
this.infoModal.clientIps = 'No IP Record';
|
||||||
this.infoModal.clientIpsArray = [];
|
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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" .}}
|
||||||
|
|
@ -175,12 +138,27 @@
|
||||||
style="margin-bottom: -10px; position: relative; z-index: 2; box-shadow: 0 2px 4px rgba(0,0,0,0.2);">
|
style="margin-bottom: -10px; position: relative; z-index: 2; box-shadow: 0 2px 4px rgba(0,0,0,0.2);">
|
||||||
<span>[[ linkName(link, idx) ]]</span>
|
<span>[[ linkName(link, idx) ]]</span>
|
||||||
</a-tag>
|
</a-tag>
|
||||||
<div @click="copy(link)" class="subscription-link-box">
|
<div @click="copy(link)" style="
|
||||||
|
cursor: pointer;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 25px 20px 15px 20px;
|
||||||
|
margin-top: -12px;
|
||||||
|
word-break: break-all;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: left;
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||||
|
transition: all 0.3s;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
" onmouseover="this.style.background='rgba(0, 0, 0, 0.3)'; this.style.borderColor='rgba(255, 255, 255, 0.2)'"
|
||||||
|
onmouseout="this.style.background='rgba(0, 0, 0, 0.2)'; this.style.borderColor='rgba(255, 255, 255, 0.1)'">
|
||||||
[[ link ]]
|
[[ link ]]
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -4,22 +4,17 @@
|
||||||
<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
|
<span v-if="!isMobile">{{ i18n "pages.xray.outbound.addOutbound" }}</span>
|
||||||
"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}}
|
||||||
|
|
@ -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: '',
|
||||||
|
|
@ -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"}}',
|
||||||
|
|
@ -1079,7 +981,7 @@
|
||||||
|
|
||||||
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: {
|
||||||
|
|
|
||||||
|
|
@ -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{}
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ func (j *XrayTrafficJob) Run() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast traffic update via WebSocket with accumulated values from database
|
// Broadcast traffic update via WebSocket with accumulated values from database
|
||||||
trafficUpdate := map[string]any{
|
trafficUpdate := map[string]interface{}{
|
||||||
"traffics": traffics,
|
"traffics": traffics,
|
||||||
"clientTraffics": clientTraffics,
|
"clientTraffics": clientTraffics,
|
||||||
"onlineClients": onlineClients,
|
"onlineClients": onlineClients,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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://"
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 المدمج"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -201,7 +201,7 @@
|
||||||
"destinationPort" = "Puerto de Destino"
|
"destinationPort" = "Puerto de Destino"
|
||||||
"targetAddress" = "Dirección de Destino"
|
"targetAddress" = "Dirección de Destino"
|
||||||
"monitorDesc" = "Dejar en blanco por defecto"
|
"monitorDesc" = "Dejar en blanco por defecto"
|
||||||
"meansNoLimit" = " = illimitata. (unidad: GB)"
|
"meansNoLimit" = "= illimitata. (unidad: GB)"
|
||||||
"totalFlow" = "Flujo Total"
|
"totalFlow" = "Flujo Total"
|
||||||
"leaveBlankToNeverExpire" = "Dejar en Blanco para Nunca Expirar"
|
"leaveBlankToNeverExpire" = "Dejar en Blanco para Nunca Expirar"
|
||||||
"noRecommendKeepDefault" = "No hay requisitos especiales para mantener la configuración predeterminada"
|
"noRecommendKeepDefault" = "No hay requisitos especiales para mantener la configuración predeterminada"
|
||||||
|
|
@ -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!"
|
||||||
|
|
|
||||||
|
|
@ -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" = "سرور حل دامنه داخلی را فعال کنید"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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サーバーを有効にする"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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-сервер"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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-сервер"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 服务器"
|
||||||
|
|
|
||||||
|
|
@ -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 伺服器"
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ func BroadcastInbounds(inbounds any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// BroadcastOutbounds broadcasts outbounds list update to all connected clients
|
// BroadcastOutbounds broadcasts outbounds list update to all connected clients
|
||||||
func BroadcastOutbounds(outbounds any) {
|
func BroadcastOutbounds(outbounds interface{}) {
|
||||||
hub := GetHub()
|
hub := GetHub()
|
||||||
if hub != nil {
|
if hub != nil {
|
||||||
hub.Broadcast(MessageTypeOutbounds, outbounds)
|
hub.Broadcast(MessageTypeOutbounds, outbounds)
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
184
x-ui.sh
184
x-ui.sh
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -530,27 +513,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 +534,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 +903,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 +931,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
|
||||||
;;
|
;;
|
||||||
|
|
@ -1186,47 +1181,16 @@ ssl_cert_issue_for_ip() {
|
||||||
LOGI "Including IPv6 address: ${ipv6_addr}"
|
LOGI "Including IPv6 address: ${ipv6_addr}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Choose port for HTTP-01 listener (default 80, allow override)
|
# Use port 80 for certificate issuance
|
||||||
local WebPort=""
|
local WebPort=80
|
||||||
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}"
|
LOGI "Using port ${WebPort} to issue certificate for IP: ${server_ip}"
|
||||||
if [[ "${WebPort}" -ne 80 ]]; then
|
LOGI "Make sure port ${WebPort} is open and not in use..."
|
||||||
LOGI "Reminder: Let's Encrypt still reaches port 80; forward external port 80 to ${WebPort} for validation."
|
|
||||||
fi
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
if is_port_in_use "${WebPort}"; then
|
|
||||||
LOGI "Port ${WebPort} is currently in use."
|
|
||||||
|
|
||||||
local alt_port=""
|
|
||||||
read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port
|
|
||||||
alt_port="${alt_port// /}"
|
|
||||||
if [[ -z "${alt_port}" ]]; then
|
|
||||||
LOGE "Port ${WebPort} is busy; cannot proceed with issuance."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then
|
|
||||||
LOGE "Invalid port provided."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
WebPort="${alt_port}"
|
|
||||||
continue
|
|
||||||
else
|
|
||||||
LOGI "Port ${WebPort} is free and ready for standalone validation."
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Reload command - restarts panel after renewal
|
# Reload command - restarts panel after renewal
|
||||||
local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null"
|
local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null"
|
||||||
|
|
||||||
# issue the certificate for IP with shortlived profile
|
# issue the certificate for IP with shortlived profile
|
||||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force
|
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
|
||||||
~/.acme.sh/acme.sh --issue \
|
~/.acme.sh/acme.sh --issue \
|
||||||
${domain_args} \
|
${domain_args} \
|
||||||
--standalone \
|
--standalone \
|
||||||
|
|
@ -1391,7 +1355,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."
|
||||||
|
|
@ -1518,7 +1482,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
|
||||||
|
|
@ -2062,15 +2026,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}')
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue