mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-02-13 13:57:59 +00:00
Compare commits
61 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5c0fe3edf | ||
|
|
f4057989f5 | ||
|
|
84013b0b3f | ||
|
|
511adffc5b | ||
|
|
fc6344b840 | ||
|
|
b3555ce1b8 | ||
|
|
c2f409c3c4 | ||
|
|
0994f8756f | ||
|
|
4779939424 | ||
|
|
4a455aa532 | ||
|
|
25f64738e4 | ||
|
|
5bb87fd3d4 | ||
|
|
491e3f9f8b | ||
|
|
d8fb09faae | ||
|
|
f87c68ea68 | ||
|
|
687e8cf1ba | ||
|
|
03f04194f2 | ||
|
|
248700a8a3 | ||
|
|
ff128a7275 | ||
|
|
e8d2973be7 | ||
|
|
f3d47ebb3f | ||
|
|
06c49b92f8 | ||
|
|
e35213bc73 | ||
|
|
aa6a886977 | ||
|
|
9d603c5ad2 | ||
|
|
a973fa6d68 | ||
|
|
3af6497577 | ||
|
|
c59f54bb0e | ||
|
|
6b3da4fe5e | ||
|
|
ea0da32e81 | ||
|
|
d5ea8d0f38 | ||
|
|
fd5f591737 | ||
|
|
8a4c9a98cb | ||
|
|
70b365171f | ||
|
|
328ba3b45e | ||
|
|
5370b6943a | ||
|
|
d8c783a296 | ||
|
|
809f69729a | ||
|
|
93b7ce199f | ||
|
|
2a76cec804 | ||
|
|
88eab032be | ||
|
|
20ec863f51 | ||
|
|
2f4018bbe5 | ||
|
|
f273708f6d | ||
|
|
e6318d57e4 | ||
|
|
77fa976ee9 | ||
|
|
8098d2b1b1 | ||
|
|
a691eaea8d | ||
|
|
da447e5669 | ||
|
|
f8c9aac97c | ||
|
|
e42c17f2b2 | ||
|
|
427b7b67d8 | ||
|
|
ccf08086ac | ||
|
|
7b0a3929ff | ||
|
|
570ab8e5e0 | ||
|
|
1240e4c962 | ||
|
|
c117b8b272 | ||
|
|
6041d10e3d | ||
|
|
4800f8fb70 | ||
|
|
a9770e1da2 | ||
|
|
3f15d21f13 |
82 changed files with 4845 additions and 1762 deletions
155
.github/copilot-instructions.md
vendored
Normal file
155
.github/copilot-instructions.md
vendored
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
# 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
Normal file
31
.github/workflows/cleanup_caches.yml
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
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
|
||||
43
.github/workflows/release.yml
vendored
43
.github/workflows/release.yml
vendored
|
|
@ -18,6 +18,7 @@ on:
|
|||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'x-ui.service.debian'
|
||||
- 'x-ui.service.arch'
|
||||
- 'x-ui.service.rhel'
|
||||
|
||||
jobs:
|
||||
|
|
@ -80,6 +81,7 @@ jobs:
|
|||
mkdir x-ui
|
||||
cp xui-release x-ui/
|
||||
cp x-ui.service.debian x-ui/
|
||||
cp x-ui.service.arch x-ui/
|
||||
cp x-ui.service.rhel x-ui/
|
||||
cp x-ui.sh x-ui/
|
||||
mv x-ui/xui-release x-ui/x-ui
|
||||
|
|
@ -87,7 +89,7 @@ jobs:
|
|||
cd x-ui/bin
|
||||
|
||||
# Download dependencies
|
||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.12.8/"
|
||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v26.2.6/"
|
||||
if [ "${{ matrix.platform }}" == "amd64" ]; then
|
||||
wget -q ${Xray_URL}Xray-linux-64.zip
|
||||
unzip Xray-linux-64.zip
|
||||
|
|
@ -171,21 +173,42 @@ jobs:
|
|||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
- name: Build 3X-UI for Windows
|
||||
- name: Install MSYS2
|
||||
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
|
||||
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
|
||||
Copy-Item xui-release.exe x-ui\
|
||||
Copy-Item xui-release.exe x-ui\x-ui.exe
|
||||
mkdir x-ui\bin
|
||||
cd x-ui\bin
|
||||
|
||||
# Download Xray for Windows
|
||||
$Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.12.8/"
|
||||
$Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v26.2.6/"
|
||||
Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
|
||||
Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
|
||||
Remove-Item "Xray-windows-64.zip"
|
||||
|
|
@ -223,4 +246,4 @@ jobs:
|
|||
file: x-ui-windows-amd64.zip
|
||||
asset_name: x-ui-windows-amd64.zip
|
||||
overwrite: true
|
||||
prerelease: true
|
||||
prerelease: true
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ case $1 in
|
|||
esac
|
||||
mkdir -p build/bin
|
||||
cd build/bin
|
||||
curl -sfLRO "https://github.com/XTLS/Xray-core/releases/download/v25.12.8/Xray-linux-${ARCH}.zip"
|
||||
curl -sfLRO "https://github.com/XTLS/Xray-core/releases/download/v26.2.6/Xray-linux-${ARCH}.zip"
|
||||
unzip "Xray-linux-${ARCH}.zip"
|
||||
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
|
||||
mv xray "xray-linux-${FNAME}"
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ RUN apk add --no-cache --update \
|
|||
ca-certificates \
|
||||
tzdata \
|
||||
fail2ban \
|
||||
bash
|
||||
bash \
|
||||
curl \
|
||||
openssl
|
||||
|
||||
COPY --from=builder /app/build/ /app/
|
||||
COPY --from=builder /app/DockerEntrypoint.sh /app/
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
2.8.6
|
||||
2.8.10
|
||||
|
|
@ -80,9 +80,12 @@ type HistoryOfSeeders struct {
|
|||
// GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.
|
||||
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
||||
listen := i.Listen
|
||||
if listen != "" {
|
||||
listen = fmt.Sprintf("\"%v\"", listen)
|
||||
// Default to 0.0.0.0 (all interfaces) when listen is empty
|
||||
// This ensures proper dual-stack IPv4/IPv6 binding in systems where bindv6only=0
|
||||
if listen == "" {
|
||||
listen = "0.0.0.0"
|
||||
}
|
||||
listen = fmt.Sprintf("\"%v\"", listen)
|
||||
return &xray.InboundConfig{
|
||||
Listen: json_util.RawMessage(listen),
|
||||
Port: i.Port,
|
||||
|
|
|
|||
58
go.mod
58
go.mod
|
|
@ -1,6 +1,6 @@
|
|||
module github.com/mhsanaei/3x-ui/v2
|
||||
|
||||
go 1.25.5
|
||||
go 1.25.7
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/gzip v1.2.5
|
||||
|
|
@ -11,20 +11,20 @@ require (
|
|||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mymmrac/telego v1.4.0
|
||||
github.com/mymmrac/telego v1.6.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v4 v4.25.12
|
||||
github.com/shirou/gopsutil/v4 v4.26.1
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/valyala/fasthttp v1.68.0
|
||||
github.com/valyala/fasthttp v1.69.0
|
||||
github.com/xlzd/gotp v0.1.0
|
||||
github.com/xtls/xray-core v1.251208.0
|
||||
github.com/xtls/xray-core v1.260206.0
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/sys v0.39.0
|
||||
golang.org/x/text v0.32.0
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/sys v0.41.0
|
||||
golang.org/x/text v0.34.0
|
||||
google.golang.org/grpc v1.78.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
|
|
@ -33,21 +33,21 @@ require (
|
|||
require (
|
||||
github.com/Azure/go-ntlmssp v0.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.14.2 // indirect
|
||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.2 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // 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-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/goccy/go-yaml v1.19.1 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
|
|
@ -57,30 +57,27 @@ require (
|
|||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/juju/ratelimit v1.0.2 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.33 // indirect
|
||||
github.com/miekg/dns v1.1.69 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.34 // indirect
|
||||
github.com/miekg/dns v1.1.72 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/pires/go-proxyproto v0.11.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.58.0 // indirect
|
||||
github.com/refraction-networking/utls v1.8.1 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/refraction-networking/utls v1.8.2 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/sagernet/sing v0.7.14 // indirect
|
||||
github.com/sagernet/sing v0.7.18 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fastjson v1.6.7 // indirect
|
||||
github.com/vishvananda/netlink v1.3.1 // indirect
|
||||
|
|
@ -88,16 +85,17 @@ require (
|
|||
github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/arch v0.24.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260209203927-2842357ff358 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
|
||||
lukechampine.com/blake3 v1.4.1 // indirect
|
||||
)
|
||||
|
|
|
|||
115
go.sum
115
go.sum
|
|
@ -6,26 +6,25 @@ github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktp
|
|||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178 h1:bSq8n+gX4oO/qnM3MKf4kroW75n+phO9Qp6nigJKZ1E=
|
||||
github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178/go.mod h1:N1WIjPphkqs4efXWuyDNQ6OjjIK04vM3h+bEgwV+eVU=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
||||
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
||||
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ=
|
||||
github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM=
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/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/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI=
|
||||
|
|
@ -57,8 +56,8 @@ github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy0
|
|||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
|
||||
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
|
||||
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
|
|
@ -106,8 +105,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
|
|||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
|
||||
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
|
|
@ -120,17 +119,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/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
||||
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
|
||||
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mymmrac/telego v1.4.0 h1:z74W5lfOTgLplQXuZPjDsRvvvI0iQatO2gp/XZz7s3I=
|
||||
github.com/mymmrac/telego v1.4.0/go.mod h1:u9fKXZSOCOdMj6K0U69fQqeAvDE+2RGkHKkDksijp3o=
|
||||
github.com/mymmrac/telego v1.6.0 h1:Zc8rgyHozvd/7ZgyrigyHdAF9koHYMfilYfyB6wlFC0=
|
||||
github.com/mymmrac/telego v1.6.0/go.mod h1:xt6ZWA8zi8KmuzryE1ImEdl9JSwjHNpM4yhC7D8hU4Y=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
|
|
@ -139,32 +138,28 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
|
|||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
|
||||
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
|
||||
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo=
|
||||
github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
||||
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sagernet/sing v0.7.14 h1:5QQRDCUvYNOMyVp3LuK/hYEBAIv0VsbD3x/l9zH467s=
|
||||
github.com/sagernet/sing v0.7.14/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing v0.7.18 h1:iZHkaru1/MoHugx3G+9S3WG4owMewKO/KvieE2Pzk4E=
|
||||
github.com/sagernet/sing v0.7.18/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
||||
github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
|
||||
github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
|
||||
github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo=
|
||||
github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
|
|
@ -172,7 +167,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
|
|||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
|
|
@ -187,12 +181,10 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM=
|
||||
github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
|
|
@ -203,8 +195,8 @@ github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
|
|||
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
|
||||
github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 h1:UXjrmniKlY+ZbIqpN91lejB3pszQQQRVu1vqH/p/aGM=
|
||||
github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237/go.mod h1:vbHCV/3VWUvy1oKvTxxWJRPEWSeR1sYgQHIh6u/JiZQ=
|
||||
github.com/xtls/xray-core v1.251208.0 h1:9jIXi+9KXnfmT5esSYNf9VAQlQkaAP8bG413B0eyAes=
|
||||
github.com/xtls/xray-core v1.251208.0/go.mod h1:kclzboEF0g6VBrp9/NXm8C0Aj64SDBt52OfthH1LSr4=
|
||||
github.com/xtls/xray-core v1.260206.0 h1:gY8IV6u76CW93txL9QmacgZ0Udxr2Q3e9qUxXAhdHqI=
|
||||
github.com/xtls/xray-core v1.260206.0/go.mod h1:GyFIgVGRJkt3eyV/NMcdxOKXcJPqGGpyupHzy16uJhU=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
|
|
@ -229,14 +221,16 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
|||
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/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
||||
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20260209203927-2842357ff358 h1:kpfSV7uLwKJbFSEgNhWzGSL47NDSF/5pYYQw1V0ub6c=
|
||||
golang.org/x/exp v0.0.0-20260209203927-2842357ff358/go.mod h1:R3t0oliuryB5eenPWl3rrQxwnNM3WTwnsRZZiXLAAW8=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
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/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
@ -245,22 +239,22 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
|
@ -271,14 +265,13 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
|||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
|
||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h1:Lk6hARj5UPY47dBep70OD/TIMwikJ5fGUGX0Rm3Xigk=
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
|
||||
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
||||
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
|
||||
|
|
|
|||
366
install.sh
366
install.sh
|
|
@ -53,35 +53,52 @@ is_ip() {
|
|||
is_ipv4 "$1" || is_ipv6 "$1"
|
||||
}
|
||||
is_domain() {
|
||||
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[A-Za-z]{2,}$ ]] && return 0 || return 1
|
||||
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1
|
||||
}
|
||||
|
||||
# Port helpers
|
||||
is_port_in_use() {
|
||||
local port="$1"
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}'
|
||||
return
|
||||
fi
|
||||
if command -v netstat >/dev/null 2>&1; then
|
||||
netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}'
|
||||
return
|
||||
fi
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
install_base() {
|
||||
case "${release}" in
|
||||
ubuntu | debian | armbian)
|
||||
apt-get update && apt-get install -y -q curl tar tzdata openssl socat
|
||||
apt-get update && apt-get install -y -q curl tar tzdata socat ca-certificates
|
||||
;;
|
||||
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
|
||||
dnf -y update && dnf install -y -q curl tar tzdata openssl socat
|
||||
dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates
|
||||
;;
|
||||
centos)
|
||||
if [[ "${VERSION_ID}" =~ ^7 ]]; then
|
||||
yum -y update && yum install -y curl tar tzdata openssl socat
|
||||
yum -y update && yum install -y curl tar tzdata socat ca-certificates
|
||||
else
|
||||
dnf -y update && dnf install -y -q curl tar tzdata openssl socat
|
||||
dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates
|
||||
fi
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
pacman -Syu && pacman -Syu --noconfirm curl tar tzdata openssl socat
|
||||
pacman -Syu && pacman -Syu --noconfirm curl tar tzdata socat ca-certificates
|
||||
;;
|
||||
opensuse-tumbleweed | opensuse-leap)
|
||||
zypper refresh && zypper -q install -y curl tar timezone openssl socat
|
||||
zypper refresh && zypper -q install -y curl tar timezone socat ca-certificates
|
||||
;;
|
||||
alpine)
|
||||
apk update && apk add curl tar tzdata openssl socat
|
||||
apk update && apk add curl tar tzdata socat ca-certificates
|
||||
;;
|
||||
*)
|
||||
apt-get update && apt-get install -y -q curl tar tzdata openssl socat
|
||||
apt-get update && apt-get install -y -q curl tar tzdata socat ca-certificates
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
|
@ -130,7 +147,7 @@ setup_ssl_certificate() {
|
|||
echo -e "${green}Issuing SSL certificate for ${domain}...${plain}"
|
||||
echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}"
|
||||
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1
|
||||
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
|
|
@ -154,7 +171,9 @@ setup_ssl_certificate() {
|
|||
|
||||
# Enable auto-renew
|
||||
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
|
||||
chmod 755 $certPath/* 2>/dev/null
|
||||
# Secure permissions: private key readable only by owner
|
||||
chmod 600 $certPath/privkey.pem 2>/dev/null
|
||||
chmod 644 $certPath/fullchain.pem 2>/dev/null
|
||||
|
||||
# Set certificate for panel
|
||||
local webCertFile="/root/cert/${domain}/fullchain.pem"
|
||||
|
|
@ -170,56 +189,155 @@ setup_ssl_certificate() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Fallback: generate a self-signed certificate (not publicly trusted)
|
||||
setup_self_signed_certificate() {
|
||||
local name="$1" # domain or IP to place in SAN
|
||||
local certDir="/root/cert/selfsigned"
|
||||
# Issue Let's Encrypt IP certificate with shortlived profile (~6 days validity)
|
||||
# Requires acme.sh and port 80 open for HTTP-01 challenge
|
||||
setup_ip_certificate() {
|
||||
local ipv4="$1"
|
||||
local ipv6="$2" # optional
|
||||
|
||||
echo -e "${yellow}Generating a self-signed certificate (not publicly trusted)...${plain}"
|
||||
echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}"
|
||||
echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}"
|
||||
echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}"
|
||||
|
||||
mkdir -p "$certDir"
|
||||
|
||||
local sanExt=""
|
||||
if is_ip "$name"; then
|
||||
sanExt="IP:${name}"
|
||||
else
|
||||
sanExt="DNS:${name}"
|
||||
# Check for acme.sh
|
||||
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||
install_acme
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${red}Failed to install acme.sh${plain}"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Use -addext if supported; fallback to config file if needed
|
||||
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
|
||||
-keyout "${certDir}/privkey.pem" \
|
||||
-out "${certDir}/fullchain.pem" \
|
||||
-subj "/CN=${name}" \
|
||||
-addext "subjectAltName=${sanExt}" >/dev/null 2>&1
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
# Fallback via temporary config file (for older OpenSSL versions)
|
||||
local tmpCfg="${certDir}/openssl.cnf"
|
||||
cat > "$tmpCfg" <<EOF
|
||||
[req]
|
||||
distinguished_name=req_distinguished_name
|
||||
req_extensions=v3_req
|
||||
[req_distinguished_name]
|
||||
[v3_req]
|
||||
subjectAltName=${sanExt}
|
||||
EOF
|
||||
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
|
||||
-keyout "${certDir}/privkey.pem" \
|
||||
-out "${certDir}/fullchain.pem" \
|
||||
-subj "/CN=${name}" \
|
||||
-config "$tmpCfg" -extensions v3_req >/dev/null 2>&1
|
||||
rm -f "$tmpCfg"
|
||||
fi
|
||||
|
||||
if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then
|
||||
echo -e "${red}Failed to generate self-signed certificate${plain}"
|
||||
# Validate IP address
|
||||
if [[ -z "$ipv4" ]]; then
|
||||
echo -e "${red}IPv4 address is required${plain}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
chmod 755 ${certDir}/* 2>/dev/null
|
||||
${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" >/dev/null 2>&1
|
||||
echo -e "${yellow}Self-signed certificate configured. Browsers will show a warning.${plain}"
|
||||
if ! is_ipv4 "$ipv4"; then
|
||||
echo -e "${red}Invalid IPv4 address: $ipv4${plain}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Create certificate directory
|
||||
local certDir="/root/cert/ip"
|
||||
mkdir -p "$certDir"
|
||||
|
||||
# Build domain arguments
|
||||
local domain_args="-d ${ipv4}"
|
||||
if [[ -n "$ipv6" ]] && is_ipv6 "$ipv6"; then
|
||||
domain_args="${domain_args} -d ${ipv6}"
|
||||
echo -e "${green}Including IPv6 address: ${ipv6}${plain}"
|
||||
fi
|
||||
|
||||
# Set reload command for auto-renewal (add || true so it doesn't fail during first install)
|
||||
local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null || true"
|
||||
|
||||
# Choose port for HTTP-01 listener (default 80, prompt override)
|
||||
local WebPort=""
|
||||
read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort
|
||||
WebPort="${WebPort:-80}"
|
||||
if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then
|
||||
echo -e "${red}Invalid port provided. Falling back to 80.${plain}"
|
||||
WebPort=80
|
||||
fi
|
||||
echo -e "${green}Using port ${WebPort} for standalone validation.${plain}"
|
||||
if [[ "${WebPort}" -ne 80 ]]; then
|
||||
echo -e "${yellow}Reminder: Let's Encrypt still connects on port 80; forward external port 80 to ${WebPort}.${plain}"
|
||||
fi
|
||||
|
||||
# Ensure chosen port is available
|
||||
while true; do
|
||||
if is_port_in_use "${WebPort}"; then
|
||||
echo -e "${yellow}Port ${WebPort} is in use.${plain}"
|
||||
|
||||
local alt_port=""
|
||||
read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port
|
||||
alt_port="${alt_port// /}"
|
||||
if [[ -z "${alt_port}" ]]; then
|
||||
echo -e "${red}Port ${WebPort} is busy; cannot proceed.${plain}"
|
||||
return 1
|
||||
fi
|
||||
if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then
|
||||
echo -e "${red}Invalid port provided.${plain}"
|
||||
return 1
|
||||
fi
|
||||
WebPort="${alt_port}"
|
||||
continue
|
||||
else
|
||||
echo -e "${green}Port ${WebPort} is free and ready for standalone validation.${plain}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Issue certificate with shortlived profile
|
||||
echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}"
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1
|
||||
|
||||
~/.acme.sh/acme.sh --issue \
|
||||
${domain_args} \
|
||||
--standalone \
|
||||
--server letsencrypt \
|
||||
--certificate-profile shortlived \
|
||||
--days 6 \
|
||||
--httpport ${WebPort} \
|
||||
--force
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${red}Failed to issue IP certificate${plain}"
|
||||
echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}"
|
||||
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
|
||||
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
|
||||
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
|
||||
rm -rf ${certDir} 2>/dev/null
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "${green}Certificate issued successfully, installing...${plain}"
|
||||
|
||||
# Install certificate
|
||||
# Note: acme.sh may report "Reload error" and exit non-zero if reloadcmd fails,
|
||||
# but the cert files are still installed. We check for files instead of exit code.
|
||||
~/.acme.sh/acme.sh --installcert -d ${ipv4} \
|
||||
--key-file "${certDir}/privkey.pem" \
|
||||
--fullchain-file "${certDir}/fullchain.pem" \
|
||||
--reloadcmd "${reloadCmd}" 2>&1 || true
|
||||
|
||||
# Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero)
|
||||
if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then
|
||||
echo -e "${red}Certificate files not found after installation${plain}"
|
||||
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
|
||||
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
|
||||
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
|
||||
rm -rf ${certDir} 2>/dev/null
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "${green}Certificate files installed successfully${plain}"
|
||||
|
||||
# Enable auto-upgrade for acme.sh (ensures cron job runs)
|
||||
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
|
||||
|
||||
# Secure permissions: private key readable only by owner
|
||||
chmod 600 ${certDir}/privkey.pem 2>/dev/null
|
||||
chmod 644 ${certDir}/fullchain.pem 2>/dev/null
|
||||
|
||||
# Configure panel to use the certificate
|
||||
echo -e "${green}Setting certificate paths for the panel...${plain}"
|
||||
${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${yellow}Warning: Could not set certificate paths automatically${plain}"
|
||||
echo -e "${yellow}Certificate files are at:${plain}"
|
||||
echo -e " Cert: ${certDir}/fullchain.pem"
|
||||
echo -e " Key: ${certDir}/privkey.pem"
|
||||
else
|
||||
echo -e "${green}Certificate paths configured successfully${plain}"
|
||||
fi
|
||||
|
||||
echo -e "${green}IP certificate installed and configured successfully!${plain}"
|
||||
echo -e "${green}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}"
|
||||
echo -e "${yellow}acme.sh will automatically renew and reload x-ui before expiry.${plain}"
|
||||
return 0
|
||||
}
|
||||
|
||||
|
|
@ -296,7 +414,7 @@ ssl_cert_issue() {
|
|||
systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null
|
||||
|
||||
# issue the certificate
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force
|
||||
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${red}Issuing certificate failed, please check logs.${plain}"
|
||||
|
|
@ -352,14 +470,18 @@ ssl_cert_issue() {
|
|||
if [ $? -ne 0 ]; then
|
||||
echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}"
|
||||
ls -lah /root/cert/${domain}/
|
||||
chmod 755 $certPath/*
|
||||
# Secure permissions: private key readable only by owner
|
||||
chmod 600 $certPath/privkey.pem 2>/dev/null
|
||||
chmod 644 $certPath/fullchain.pem 2>/dev/null
|
||||
else
|
||||
echo -e "${green}Auto renew succeeded, certificate details:${plain}"
|
||||
ls -lah /root/cert/${domain}/
|
||||
chmod 755 $certPath/*
|
||||
# Secure permissions: private key readable only by owner
|
||||
chmod 600 $certPath/privkey.pem 2>/dev/null
|
||||
chmod 644 $certPath/fullchain.pem 2>/dev/null
|
||||
fi
|
||||
|
||||
# Restart panel
|
||||
# start panel
|
||||
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
|
||||
|
||||
# Prompt user to set panel paths after successful certificate installation
|
||||
|
|
@ -387,7 +509,7 @@ ssl_cert_issue() {
|
|||
return 0
|
||||
}
|
||||
|
||||
# Reusable interactive SSL setup (domain or self-signed)
|
||||
# Reusable interactive SSL setup (domain or IP)
|
||||
# Sets global `SSL_HOST` to the chosen domain/IP for Access URL usage
|
||||
prompt_and_setup_ssl() {
|
||||
local panel_port="$1"
|
||||
|
|
@ -397,20 +519,22 @@ prompt_and_setup_ssl() {
|
|||
local ssl_choice=""
|
||||
|
||||
echo -e "${yellow}Choose SSL certificate setup method:${plain}"
|
||||
echo -e "${green}1.${plain} Let's Encrypt (domain required, recommended)"
|
||||
echo -e "${green}2.${plain} Self-signed certificate (not publicly trusted)"
|
||||
read -rp "Choose an option (default 2): " ssl_choice
|
||||
echo -e "${green}1.${plain} Let's Encrypt for Domain (90-day validity, auto-renews)"
|
||||
echo -e "${green}2.${plain} Let's Encrypt for IP Address (6-day validity, auto-renews)"
|
||||
echo -e "${green}3.${plain} Custom SSL Certificate (Path to existing files)"
|
||||
echo -e "${blue}Note:${plain} Options 1 & 2 require port 80 open. Option 3 requires manual paths."
|
||||
read -rp "Choose an option (default 2 for IP): " ssl_choice
|
||||
ssl_choice="${ssl_choice// /}" # Trim whitespace
|
||||
|
||||
# Default to 2 (self-signed) if not 1
|
||||
if [[ "$ssl_choice" != "1" ]]; then
|
||||
# Default to 2 (IP cert) if input is empty or invalid (not 1 or 3)
|
||||
if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" ]]; then
|
||||
ssl_choice="2"
|
||||
fi
|
||||
|
||||
case "$ssl_choice" in
|
||||
1)
|
||||
# User chose Let's Encrypt domain option
|
||||
echo -e "${green}Using ssl_cert_issue() for comprehensive domain setup...${plain}"
|
||||
echo -e "${green}Using Let's Encrypt for domain certificate...${plain}"
|
||||
ssl_cert_issue
|
||||
# Extract the domain that was used from the certificate
|
||||
local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}')
|
||||
|
|
@ -423,28 +547,89 @@ prompt_and_setup_ssl() {
|
|||
fi
|
||||
;;
|
||||
2)
|
||||
# User chose self-signed option
|
||||
# Stop panel if running
|
||||
# User chose Let's Encrypt IP certificate option
|
||||
echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}"
|
||||
|
||||
# Ask for optional IPv6
|
||||
local ipv6_addr=""
|
||||
read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr
|
||||
ipv6_addr="${ipv6_addr// /}" # Trim whitespace
|
||||
|
||||
# Stop panel if running (port 80 needed)
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service x-ui stop >/dev/null 2>&1
|
||||
else
|
||||
systemctl stop x-ui >/dev/null 2>&1
|
||||
fi
|
||||
echo -e "${yellow}Using server IP for self-signed certificate: ${server_ip}${plain}"
|
||||
setup_self_signed_certificate "${server_ip}"
|
||||
|
||||
setup_ip_certificate "${server_ip}" "${ipv6_addr}"
|
||||
if [ $? -eq 0 ]; then
|
||||
SSL_HOST="${server_ip}"
|
||||
echo -e "${green}✓ Self-signed SSL configured successfully${plain}"
|
||||
echo -e "${green}✓ Let's Encrypt IP certificate configured successfully${plain}"
|
||||
else
|
||||
echo -e "${red}✗ Self-signed SSL setup failed${plain}"
|
||||
echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}"
|
||||
SSL_HOST="${server_ip}"
|
||||
fi
|
||||
# Start panel after SSL is configured
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service x-ui start >/dev/null 2>&1
|
||||
;;
|
||||
3)
|
||||
# User chose Custom Paths (User Provided) option
|
||||
echo -e "${green}Using custom existing certificate...${plain}"
|
||||
local custom_cert=""
|
||||
local custom_key=""
|
||||
local custom_domain=""
|
||||
|
||||
# 3.1 Request Domain to compose Panel URL later
|
||||
read -rp "Please enter domain name certificate issued for: " custom_domain
|
||||
custom_domain="${custom_domain// /}" # Убираем пробелы
|
||||
|
||||
# 3.2 Loop for Certificate Path
|
||||
while true; do
|
||||
read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert
|
||||
# Strip quotes if present
|
||||
custom_cert=$(echo "$custom_cert" | tr -d '"' | tr -d "'")
|
||||
|
||||
if [[ -f "$custom_cert" && -r "$custom_cert" && -s "$custom_cert" ]]; then
|
||||
break
|
||||
elif [[ ! -f "$custom_cert" ]]; then
|
||||
echo -e "${red}Error: File does not exist! Try again.${plain}"
|
||||
elif [[ ! -r "$custom_cert" ]]; then
|
||||
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
|
||||
else
|
||||
echo -e "${red}Error: File is empty!${plain}"
|
||||
fi
|
||||
done
|
||||
|
||||
# 3.3 Loop for Private Key Path
|
||||
while true; do
|
||||
read -rp "Input private key path (keywords: .key / privatekey): " custom_key
|
||||
# Strip quotes if present
|
||||
custom_key=$(echo "$custom_key" | tr -d '"' | tr -d "'")
|
||||
|
||||
if [[ -f "$custom_key" && -r "$custom_key" && -s "$custom_key" ]]; then
|
||||
break
|
||||
elif [[ ! -f "$custom_key" ]]; then
|
||||
echo -e "${red}Error: File does not exist! Try again.${plain}"
|
||||
elif [[ ! -r "$custom_key" ]]; then
|
||||
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
|
||||
else
|
||||
echo -e "${red}Error: File is empty!${plain}"
|
||||
fi
|
||||
done
|
||||
|
||||
# 3.4 Apply Settings via x-ui binary
|
||||
${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" >/dev/null 2>&1
|
||||
|
||||
# Set SSL_HOST for composing Panel URL
|
||||
if [[ -n "$custom_domain" ]]; then
|
||||
SSL_HOST="$custom_domain"
|
||||
else
|
||||
systemctl start x-ui >/dev/null 2>&1
|
||||
SSL_HOST="${server_ip}"
|
||||
fi
|
||||
|
||||
echo -e "${green}✓ Custom certificate paths applied.${plain}"
|
||||
echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}"
|
||||
|
||||
systemctl restart x-ui >/dev/null 2>&1 || rc-service x-ui restart >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
|
||||
|
|
@ -469,8 +654,11 @@ config_after_install() {
|
|||
)
|
||||
local server_ip=""
|
||||
for ip_address in "${URL_lists[@]}"; do
|
||||
server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
|
||||
if [[ -n "${server_ip}" ]]; then
|
||||
local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null)
|
||||
local http_code=$(echo "$response" | tail -n1)
|
||||
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
|
||||
if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
|
||||
server_ip="${ip_result}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
|
@ -497,7 +685,7 @@ config_after_install() {
|
|||
echo -e "${green} SSL Certificate Setup (MANDATORY) ${plain}"
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${yellow}For security, SSL certificate is required for all panels.${plain}"
|
||||
echo -e "${yellow}Let's Encrypt requires a domain name (IP certificates are not issued).${plain}"
|
||||
echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}"
|
||||
echo ""
|
||||
|
||||
prompt_and_setup_ssl "${config_port}" "${config_webBasePath}" "${server_ip}"
|
||||
|
|
@ -527,7 +715,7 @@ config_after_install() {
|
|||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${plain}"
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${yellow}Let's Encrypt requires a domain name (IP certificates are not issued).${plain}"
|
||||
echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}"
|
||||
echo ""
|
||||
prompt_and_setup_ssl "${existing_port}" "${config_webBasePath}" "${server_ip}"
|
||||
echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${config_webBasePath}${plain}"
|
||||
|
|
@ -552,7 +740,7 @@ config_after_install() {
|
|||
echo -e "${green}Username, Password, and WebBasePath are properly set.${plain}"
|
||||
fi
|
||||
|
||||
# Existing install: if no cert configured, prompt user to set domain or self-signed
|
||||
# Existing install: if no cert configured, prompt user for SSL setup
|
||||
# Properly detect empty cert by checking if cert: line exists and has content after it
|
||||
existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
|
||||
if [[ -z "$existing_cert" ]]; then
|
||||
|
|
@ -560,7 +748,7 @@ config_after_install() {
|
|||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${plain}"
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${yellow}Let's Encrypt requires a domain name (IP certificates are not issued).${plain}"
|
||||
echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}"
|
||||
echo ""
|
||||
prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}"
|
||||
echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${existing_webBasePath}${plain}"
|
||||
|
|
@ -587,7 +775,7 @@ install_x-ui() {
|
|||
fi
|
||||
fi
|
||||
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
|
||||
curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz -z ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz
|
||||
curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}Downloading x-ui failed, please be sure that your server can access GitHub ${plain}"
|
||||
exit 1
|
||||
|
|
@ -604,7 +792,7 @@ install_x-ui() {
|
|||
|
||||
url="https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz"
|
||||
echo -e "Beginning to install x-ui $1"
|
||||
curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz -z ${xui_folder}-linux-$(arch).tar.gz ${url}
|
||||
curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz ${url}
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}Download x-ui $1 failed, please check if the version exists ${plain}"
|
||||
exit 1
|
||||
|
|
@ -693,6 +881,15 @@ install_x-ui() {
|
|||
fi
|
||||
fi
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
if [ -f "x-ui.service.arch" ]; then
|
||||
echo -e "${green}Found x-ui.service.arch in extracted files, installing...${plain}"
|
||||
cp -f x-ui.service.arch ${xui_service}/x-ui.service >/dev/null 2>&1
|
||||
if [[ $? -eq 0 ]]; then
|
||||
service_installed=true
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if [ -f "x-ui.service.rhel" ]; then
|
||||
echo -e "${green}Found x-ui.service.rhel in extracted files, installing...${plain}"
|
||||
|
|
@ -712,6 +909,9 @@ install_x-ui() {
|
|||
ubuntu | debian | armbian)
|
||||
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1
|
||||
;;
|
||||
|
|
|
|||
6
main.go
6
main.go
|
|
@ -80,8 +80,8 @@ func runWebServer() {
|
|||
|
||||
// --- FIX FOR TELEGRAM BOT CONFLICT (409): Stop bot before restart ---
|
||||
service.StopBot()
|
||||
// --
|
||||
|
||||
// --
|
||||
|
||||
err := server.Stop()
|
||||
if err != nil {
|
||||
logger.Debug("Error stopping web server:", err)
|
||||
|
|
@ -113,7 +113,7 @@ func runWebServer() {
|
|||
// --- FIX FOR TELEGRAM BOT CONFLICT (409) on full shutdown ---
|
||||
service.StopBot()
|
||||
// ------------------------------------------------------------
|
||||
|
||||
|
||||
server.Stop()
|
||||
subServer.Stop()
|
||||
log.Println("Shutting down servers.")
|
||||
|
|
|
|||
28
sub/sub.go
28
sub/sub.go
|
|
@ -153,6 +153,31 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||
SubTitle = ""
|
||||
}
|
||||
|
||||
SubSupportUrl, err := s.settingService.GetSubSupportUrl()
|
||||
if err != nil {
|
||||
SubSupportUrl = ""
|
||||
}
|
||||
|
||||
SubProfileUrl, err := s.settingService.GetSubProfileUrl()
|
||||
if err != nil {
|
||||
SubProfileUrl = ""
|
||||
}
|
||||
|
||||
SubAnnounce, err := s.settingService.GetSubAnnounce()
|
||||
if err != nil {
|
||||
SubAnnounce = ""
|
||||
}
|
||||
|
||||
SubEnableRouting, err := s.settingService.GetSubEnableRouting()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
SubRoutingRules, err := s.settingService.GetSubRoutingRules()
|
||||
if err != nil {
|
||||
SubRoutingRules = ""
|
||||
}
|
||||
|
||||
// set per-request localizer from headers/cookies
|
||||
engine.Use(locale.LocalizerMiddleware())
|
||||
|
||||
|
|
@ -231,7 +256,8 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||
|
||||
s.sub = NewSUBController(
|
||||
g, LinksPath, JsonPath, subJsonEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates,
|
||||
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle)
|
||||
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle, SubSupportUrl,
|
||||
SubProfileUrl, SubAnnounce, SubEnableRouting, SubRoutingRules)
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package sub
|
|||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/config"
|
||||
|
|
@ -12,12 +13,17 @@ import (
|
|||
|
||||
// SUBController handles HTTP requests for subscription links and JSON configurations.
|
||||
type SUBController struct {
|
||||
subTitle string
|
||||
subPath string
|
||||
subJsonPath string
|
||||
jsonEnabled bool
|
||||
subEncrypt bool
|
||||
updateInterval string
|
||||
subTitle string
|
||||
subSupportUrl string
|
||||
subProfileUrl string
|
||||
subAnnounce string
|
||||
subEnableRouting bool
|
||||
subRoutingRules string
|
||||
subPath string
|
||||
subJsonPath string
|
||||
jsonEnabled bool
|
||||
subEncrypt bool
|
||||
updateInterval string
|
||||
|
||||
subService *SubService
|
||||
subJsonService *SubJsonService
|
||||
|
|
@ -38,15 +44,25 @@ func NewSUBController(
|
|||
jsonMux string,
|
||||
jsonRules string,
|
||||
subTitle string,
|
||||
subSupportUrl string,
|
||||
subProfileUrl string,
|
||||
subAnnounce string,
|
||||
subEnableRouting bool,
|
||||
subRoutingRules string,
|
||||
) *SUBController {
|
||||
sub := NewSubService(showInfo, rModel)
|
||||
a := &SUBController{
|
||||
subTitle: subTitle,
|
||||
subPath: subPath,
|
||||
subJsonPath: jsonPath,
|
||||
jsonEnabled: jsonEnabled,
|
||||
subEncrypt: encrypt,
|
||||
updateInterval: update,
|
||||
subTitle: subTitle,
|
||||
subSupportUrl: subSupportUrl,
|
||||
subProfileUrl: subProfileUrl,
|
||||
subAnnounce: subAnnounce,
|
||||
subEnableRouting: subEnableRouting,
|
||||
subRoutingRules: subRoutingRules,
|
||||
subPath: subPath,
|
||||
subJsonPath: jsonPath,
|
||||
jsonEnabled: jsonEnabled,
|
||||
subEncrypt: encrypt,
|
||||
updateInterval: update,
|
||||
|
||||
subService: sub,
|
||||
subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),
|
||||
|
|
@ -127,7 +143,11 @@ func (a *SUBController) subs(c *gin.Context) {
|
|||
|
||||
// Add headers
|
||||
header := fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
|
||||
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle)
|
||||
profileUrl := a.subProfileUrl
|
||||
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 {
|
||||
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
|
||||
|
|
@ -140,22 +160,54 @@ func (a *SUBController) subs(c *gin.Context) {
|
|||
// subJsons handles HTTP requests for JSON subscription configurations.
|
||||
func (a *SUBController) subJsons(c *gin.Context) {
|
||||
subId := c.Param("subid")
|
||||
_, host, _, _ := a.subService.ResolveRequest(c)
|
||||
scheme, host, hostWithPort, _ := a.subService.ResolveRequest(c)
|
||||
jsonSub, header, err := a.subJsonService.GetJson(subId, host)
|
||||
if err != nil || len(jsonSub) == 0 {
|
||||
c.String(400, "Error!")
|
||||
} else {
|
||||
|
||||
// Add headers
|
||||
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle)
|
||||
profileUrl := a.subProfileUrl
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyCommonHeaders sets common HTTP headers for subscription responses including user info, update interval, and profile title.
|
||||
func (a *SUBController) ApplyCommonHeaders(c *gin.Context, header, updateInterval, profileTitle string) {
|
||||
func (a *SUBController) ApplyCommonHeaders(
|
||||
c *gin.Context,
|
||||
header,
|
||||
updateInterval,
|
||||
profileTitle string,
|
||||
profileSupportUrl string,
|
||||
profileUrl string,
|
||||
profileAnnounce string,
|
||||
profileEnableRouting bool,
|
||||
profileRoutingRules string,
|
||||
) {
|
||||
c.Writer.Header().Set("Subscription-Userinfo", header)
|
||||
c.Writer.Header().Set("Profile-Update-Interval", updateInterval)
|
||||
c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileTitle)))
|
||||
|
||||
//Basics
|
||||
if 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"strings"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||
|
|
@ -197,9 +198,8 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
|
|||
|
||||
newOutbounds = append(newOutbounds, s.defaultOutbounds...)
|
||||
newConfigJson := make(map[string]any)
|
||||
for key, value := range s.configJson {
|
||||
newConfigJson[key] = value
|
||||
}
|
||||
maps.Copy(newConfigJson, s.configJson)
|
||||
|
||||
newConfigJson["outbounds"] = newOutbounds
|
||||
newConfigJson["remarks"] = s.SubService.genRemark(inbound, client.Email, extPrxy["remark"].(string))
|
||||
|
||||
|
|
@ -253,9 +253,6 @@ func (s *SubJsonService) tlsData(tData map[string]any) map[string]any {
|
|||
|
||||
tlsData["serverName"] = tData["serverName"]
|
||||
tlsData["alpn"] = tData["alpn"]
|
||||
if allowInsecure, ok := tlsClientSettings["allowInsecure"].(bool); ok {
|
||||
tlsData["allowInsecure"] = allowInsecure
|
||||
}
|
||||
if fingerprint, ok := tlsClientSettings["fingerprint"].(string); ok {
|
||||
tlsData["fingerprint"] = fingerprint
|
||||
}
|
||||
|
|
|
|||
|
|
@ -270,9 +270,6 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
|||
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||
obj["fp"], _ = fpValue.(string)
|
||||
}
|
||||
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||
obj["allowInsecure"], _ = insecure.(bool)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -296,7 +293,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
|||
newSecurity, _ := ep["forceTls"].(string)
|
||||
newObj := map[string]any{}
|
||||
for key, value := range obj {
|
||||
if !(newSecurity == "none" && (key == "alpn" || key == "sni" || key == "fp" || key == "allowInsecure")) {
|
||||
if !(newSecurity == "none" && (key == "alpn" || key == "sni" || key == "fp")) {
|
||||
newObj[key] = value
|
||||
}
|
||||
}
|
||||
|
|
@ -431,11 +428,6 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
|||
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||
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 {
|
||||
|
|
@ -484,8 +476,8 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
|||
externalProxies, _ := stream["externalProxy"].([]any)
|
||||
|
||||
if len(externalProxies) > 0 {
|
||||
links := ""
|
||||
for index, externalProxy := range externalProxies {
|
||||
links := make([]string, 0, len(externalProxies))
|
||||
for _, externalProxy := range externalProxies {
|
||||
ep, _ := externalProxy.(map[string]any)
|
||||
newSecurity, _ := ep["forceTls"].(string)
|
||||
dest, _ := ep["dest"].(string)
|
||||
|
|
@ -501,7 +493,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
|||
q := url.Query()
|
||||
|
||||
for k, v := range params {
|
||||
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
|
||||
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp")) {
|
||||
q.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
|
@ -511,12 +503,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
|||
|
||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
||||
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
}
|
||||
links += url.String()
|
||||
links = append(links, url.String())
|
||||
}
|
||||
return links
|
||||
return strings.Join(links, "\n")
|
||||
}
|
||||
|
||||
link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port)
|
||||
|
|
@ -635,11 +624,6 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
|||
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||
params["fp"], _ = fpValue.(string)
|
||||
}
|
||||
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||
if insecure.(bool) {
|
||||
params["allowInsecure"] = "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -701,7 +685,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
|||
q := url.Query()
|
||||
|
||||
for k, v := range params {
|
||||
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
|
||||
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp")) {
|
||||
q.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
|
@ -840,11 +824,6 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
|||
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||
params["fp"], _ = fpValue.(string)
|
||||
}
|
||||
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||
if insecure.(bool) {
|
||||
params["allowInsecure"] = "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -873,7 +852,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
|||
q := url.Query()
|
||||
|
||||
for k, v := range params {
|
||||
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
|
||||
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp")) {
|
||||
q.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
364
update.sh
364
update.sh
|
|
@ -78,7 +78,24 @@ is_ip() {
|
|||
is_ipv4 "$1" || is_ipv6 "$1"
|
||||
}
|
||||
is_domain() {
|
||||
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[A-Za-z]{2,}$ ]] && return 0 || return 1
|
||||
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1
|
||||
}
|
||||
|
||||
# Port helpers
|
||||
is_port_in_use() {
|
||||
local port="$1"
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}'
|
||||
return
|
||||
fi
|
||||
if command -v netstat >/dev/null 2>&1; then
|
||||
netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}'
|
||||
return
|
||||
fi
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
gen_random_string() {
|
||||
|
|
@ -91,29 +108,29 @@ install_base() {
|
|||
echo -e "${green}Updating and install dependency packages...${plain}"
|
||||
case "${release}" in
|
||||
ubuntu | debian | armbian)
|
||||
apt-get update >/dev/null 2>&1 && apt-get install -y -q curl tar tzdata openssl socat >/dev/null 2>&1
|
||||
apt-get update >/dev/null 2>&1 && apt-get install -y -q curl tar tzdata socat >/dev/null 2>&1
|
||||
;;
|
||||
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
|
||||
dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata openssl socat >/dev/null 2>&1
|
||||
dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata socat >/dev/null 2>&1
|
||||
;;
|
||||
centos)
|
||||
if [[ "${VERSION_ID}" =~ ^7 ]]; then
|
||||
yum -y update >/dev/null 2>&1 && yum install -y -q curl tar tzdata openssl socat >/dev/null 2>&1
|
||||
yum -y update >/dev/null 2>&1 && yum install -y -q curl tar tzdata socat >/dev/null 2>&1
|
||||
else
|
||||
dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata openssl socat >/dev/null 2>&1
|
||||
dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata socat >/dev/null 2>&1
|
||||
fi
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm curl tar tzdata openssl socat >/dev/null 2>&1
|
||||
pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm curl tar tzdata socat >/dev/null 2>&1
|
||||
;;
|
||||
opensuse-tumbleweed | opensuse-leap)
|
||||
zypper refresh >/dev/null 2>&1 && zypper -q install -y curl tar timezone openssl socat >/dev/null 2>&1
|
||||
zypper refresh >/dev/null 2>&1 && zypper -q install -y curl tar timezone socat >/dev/null 2>&1
|
||||
;;
|
||||
alpine)
|
||||
apk update >/dev/null 2>&1 && apk add curl tar tzdata openssl socat >/dev/null 2>&1
|
||||
apk update >/dev/null 2>&1 && apk add curl tar tzdata socat >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
apt-get update >/dev/null 2>&1 && apt install -y -q curl tar tzdata openssl socat >/dev/null 2>&1
|
||||
apt-get update >/dev/null 2>&1 && apt install -y -q curl tar tzdata socat >/dev/null 2>&1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
|
@ -156,7 +173,7 @@ setup_ssl_certificate() {
|
|||
echo -e "${green}Issuing SSL certificate for ${domain}...${plain}"
|
||||
echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}"
|
||||
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1
|
||||
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
|
|
@ -180,7 +197,8 @@ setup_ssl_certificate() {
|
|||
|
||||
# Enable auto-renew
|
||||
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
|
||||
chmod 755 $certPath/* 2>/dev/null
|
||||
chmod 600 $certPath/privkey.pem 2>/dev/null
|
||||
chmod 644 $certPath/fullchain.pem 2>/dev/null
|
||||
|
||||
# Set certificate for panel
|
||||
local webCertFile="/root/cert/${domain}/fullchain.pem"
|
||||
|
|
@ -196,57 +214,156 @@ setup_ssl_certificate() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Fallback: generate a self-signed certificate (not publicly trusted)
|
||||
setup_self_signed_certificate() {
|
||||
local name="$1" # domain or IP to place in SAN
|
||||
local certDir="/root/cert/selfsigned"
|
||||
# Issue Let's Encrypt IP certificate with shortlived profile (~6 days validity)
|
||||
# Requires acme.sh and port 80 open for HTTP-01 challenge
|
||||
setup_ip_certificate() {
|
||||
local ipv4="$1"
|
||||
local ipv6="$2" # optional
|
||||
|
||||
echo -e "${yellow}Generating a self-signed certificate (not publicly trusted)...${plain}"
|
||||
echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}"
|
||||
echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}"
|
||||
echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}"
|
||||
|
||||
mkdir -p "$certDir"
|
||||
|
||||
local sanExt=""
|
||||
if is_ip "$name"; then
|
||||
sanExt="IP:${name}"
|
||||
else
|
||||
sanExt="DNS:${name}"
|
||||
# Check for acme.sh
|
||||
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||
install_acme
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${red}Failed to install acme.sh${plain}"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Try -addext; fallback to config if not supported
|
||||
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
|
||||
-keyout "${certDir}/privkey.pem" \
|
||||
-out "${certDir}/fullchain.pem" \
|
||||
-subj "/CN=${name}" \
|
||||
-addext "subjectAltName=${sanExt}" >/dev/null 2>&1
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
local tmpCfg="${certDir}/openssl.cnf"
|
||||
cat > "$tmpCfg" <<EOF
|
||||
[req]
|
||||
distinguished_name=req_distinguished_name
|
||||
req_extensions=v3_req
|
||||
[req_distinguished_name]
|
||||
[v3_req]
|
||||
subjectAltName=${sanExt}
|
||||
EOF
|
||||
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
|
||||
-keyout "${certDir}/privkey.pem" \
|
||||
-out "${certDir}/fullchain.pem" \
|
||||
-subj "/CN=${name}" \
|
||||
-config "$tmpCfg" -extensions v3_req >/dev/null 2>&1
|
||||
rm -f "$tmpCfg"
|
||||
fi
|
||||
|
||||
if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then
|
||||
echo -e "${red}Failed to generate self-signed certificate${plain}"
|
||||
# Validate IP address
|
||||
if [[ -z "$ipv4" ]]; then
|
||||
echo -e "${red}IPv4 address is required${plain}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
chmod 755 ${certDir}/* 2>/dev/null
|
||||
${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" >/dev/null 2>&1
|
||||
echo -e "${yellow}Self-signed certificate configured. Browsers will show a warning.${plain}"
|
||||
if ! is_ipv4 "$ipv4"; then
|
||||
echo -e "${red}Invalid IPv4 address: $ipv4${plain}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Create certificate directory
|
||||
local certDir="/root/cert/ip"
|
||||
mkdir -p "$certDir"
|
||||
|
||||
# Build domain arguments
|
||||
local domain_args="-d ${ipv4}"
|
||||
if [[ -n "$ipv6" ]] && is_ipv6 "$ipv6"; then
|
||||
domain_args="${domain_args} -d ${ipv6}"
|
||||
echo -e "${green}Including IPv6 address: ${ipv6}${plain}"
|
||||
fi
|
||||
|
||||
# Set reload command for auto-renewal (add || true so it doesn't fail if service stopped)
|
||||
local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null || true"
|
||||
|
||||
# Choose port for HTTP-01 listener (default 80, prompt override)
|
||||
local WebPort=""
|
||||
read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort
|
||||
WebPort="${WebPort:-80}"
|
||||
if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then
|
||||
echo -e "${red}Invalid port provided. Falling back to 80.${plain}"
|
||||
WebPort=80
|
||||
fi
|
||||
echo -e "${green}Using port ${WebPort} for standalone validation.${plain}"
|
||||
if [[ "${WebPort}" -ne 80 ]]; then
|
||||
echo -e "${yellow}Reminder: Let's Encrypt still connects on port 80; forward external port 80 to ${WebPort}.${plain}"
|
||||
fi
|
||||
|
||||
# Ensure chosen port is available
|
||||
while true; do
|
||||
if is_port_in_use "${WebPort}"; then
|
||||
echo -e "${yellow}Port ${WebPort} is currently in use.${plain}"
|
||||
|
||||
local alt_port=""
|
||||
read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port
|
||||
alt_port="${alt_port// /}"
|
||||
if [[ -z "${alt_port}" ]]; then
|
||||
echo -e "${red}Port ${WebPort} is busy; cannot proceed.${plain}"
|
||||
return 1
|
||||
fi
|
||||
if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then
|
||||
echo -e "${red}Invalid port provided.${plain}"
|
||||
return 1
|
||||
fi
|
||||
WebPort="${alt_port}"
|
||||
continue
|
||||
else
|
||||
echo -e "${green}Port ${WebPort} is free and ready for standalone validation.${plain}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Issue certificate with shortlived profile
|
||||
echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}"
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1
|
||||
|
||||
~/.acme.sh/acme.sh --issue \
|
||||
${domain_args} \
|
||||
--standalone \
|
||||
--server letsencrypt \
|
||||
--certificate-profile shortlived \
|
||||
--days 6 \
|
||||
--httpport ${WebPort} \
|
||||
--force
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${red}Failed to issue IP certificate${plain}"
|
||||
echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}"
|
||||
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
|
||||
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
|
||||
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
|
||||
rm -rf ${certDir} 2>/dev/null
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "${green}Certificate issued successfully, installing...${plain}"
|
||||
|
||||
# Install certificate
|
||||
# Note: acme.sh may report "Reload error" and exit non-zero if reloadcmd fails,
|
||||
# but the cert files are still installed. We check for files instead of exit code.
|
||||
~/.acme.sh/acme.sh --installcert -d ${ipv4} \
|
||||
--key-file "${certDir}/privkey.pem" \
|
||||
--fullchain-file "${certDir}/fullchain.pem" \
|
||||
--reloadcmd "${reloadCmd}" 2>&1 || true
|
||||
|
||||
# Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero)
|
||||
if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then
|
||||
echo -e "${red}Certificate files not found after installation${plain}"
|
||||
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
|
||||
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
|
||||
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
|
||||
rm -rf ${certDir} 2>/dev/null
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "${green}Certificate files installed successfully${plain}"
|
||||
|
||||
# Enable auto-upgrade for acme.sh (ensures cron job runs)
|
||||
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
|
||||
|
||||
chmod 600 ${certDir}/privkey.pem 2>/dev/null
|
||||
chmod 644 ${certDir}/fullchain.pem 2>/dev/null
|
||||
|
||||
# Configure panel to use the certificate
|
||||
echo -e "${green}Setting certificate paths for the panel...${plain}"
|
||||
${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${yellow}Warning: Could not set certificate paths automatically.${plain}"
|
||||
echo -e "${yellow}You may need to set them manually in the panel settings.${plain}"
|
||||
echo -e "${yellow}Cert path: ${certDir}/fullchain.pem${plain}"
|
||||
echo -e "${yellow}Key path: ${certDir}/privkey.pem${plain}"
|
||||
else
|
||||
echo -e "${green}Certificate paths set successfully!${plain}"
|
||||
fi
|
||||
|
||||
echo -e "${green}IP certificate installed and configured successfully!${plain}"
|
||||
echo -e "${green}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}"
|
||||
echo -e "${yellow}Panel will automatically restart after each renewal.${plain}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Comprehensive manual SSL certificate issuance via acme.sh
|
||||
ssl_cert_issue() {
|
||||
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##')
|
||||
|
|
@ -320,7 +437,7 @@ ssl_cert_issue() {
|
|||
systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null
|
||||
|
||||
# issue the certificate
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force
|
||||
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${red}Issuing certificate failed, please check logs.${plain}"
|
||||
|
|
@ -376,11 +493,13 @@ ssl_cert_issue() {
|
|||
if [ $? -ne 0 ]; then
|
||||
echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}"
|
||||
ls -lah /root/cert/${domain}/
|
||||
chmod 755 $certPath/*
|
||||
chmod 600 $certPath/privkey.pem
|
||||
chmod 644 $certPath/fullchain.pem
|
||||
else
|
||||
echo -e "${green}Auto renew succeeded, certificate details:${plain}"
|
||||
ls -lah /root/cert/${domain}/
|
||||
chmod 755 $certPath/*
|
||||
chmod 600 $certPath/privkey.pem
|
||||
chmod 644 $certPath/fullchain.pem
|
||||
fi
|
||||
|
||||
# Restart panel
|
||||
|
|
@ -410,7 +529,7 @@ ssl_cert_issue() {
|
|||
|
||||
return 0
|
||||
}
|
||||
# Unified interactive SSL setup (domain or self-signed)
|
||||
# Unified interactive SSL setup (domain or IP)
|
||||
# Sets global `SSL_HOST` to the chosen domain/IP
|
||||
prompt_and_setup_ssl() {
|
||||
local panel_port="$1"
|
||||
|
|
@ -420,20 +539,22 @@ prompt_and_setup_ssl() {
|
|||
local ssl_choice=""
|
||||
|
||||
echo -e "${yellow}Choose SSL certificate setup method:${plain}"
|
||||
echo -e "${green}1.${plain} Let's Encrypt (domain required, recommended)"
|
||||
echo -e "${green}2.${plain} Self-signed certificate (for testing/local use)"
|
||||
read -rp "Choose an option (default 2): " ssl_choice
|
||||
echo -e "${green}1.${plain} Let's Encrypt for Domain (90-day validity, auto-renews)"
|
||||
echo -e "${green}2.${plain} Let's Encrypt for IP Address (6-day validity, auto-renews)"
|
||||
echo -e "${green}3.${plain} Custom SSL Certificate (Path to existing files)"
|
||||
echo -e "${blue}Note:${plain} Options 1 & 2 require port 80 open. Option 3 requires manual paths."
|
||||
read -rp "Choose an option (default 2 for IP): " ssl_choice
|
||||
ssl_choice="${ssl_choice// /}" # Trim whitespace
|
||||
|
||||
# Default to 2 (self-signed) if not 1
|
||||
if [[ "$ssl_choice" != "1" ]]; then
|
||||
# Default to 2 (IP cert) if input is empty or invalid (not 1 or 3)
|
||||
if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" ]]; then
|
||||
ssl_choice="2"
|
||||
fi
|
||||
|
||||
case "$ssl_choice" in
|
||||
1)
|
||||
# User chose Let's Encrypt domain option
|
||||
echo -e "${green}Using ssl_cert_issue() for comprehensive domain setup...${plain}"
|
||||
echo -e "${green}Using Let's Encrypt for domain certificate...${plain}"
|
||||
ssl_cert_issue
|
||||
# Extract the domain that was used from the certificate
|
||||
local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}')
|
||||
|
|
@ -446,32 +567,97 @@ prompt_and_setup_ssl() {
|
|||
fi
|
||||
;;
|
||||
2)
|
||||
# User chose self-signed option
|
||||
# Stop panel if running
|
||||
# User chose Let's Encrypt IP certificate option
|
||||
echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}"
|
||||
|
||||
# Ask for optional IPv6
|
||||
local ipv6_addr=""
|
||||
read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr
|
||||
ipv6_addr="${ipv6_addr// /}" # Trim whitespace
|
||||
|
||||
# Stop panel if running (port 80 needed)
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service x-ui stop >/dev/null 2>&1
|
||||
else
|
||||
systemctl stop x-ui >/dev/null 2>&1
|
||||
fi
|
||||
echo -e "${yellow}Using server IP for self-signed certificate: ${server_ip}${plain}"
|
||||
setup_self_signed_certificate "${server_ip}"
|
||||
|
||||
setup_ip_certificate "${server_ip}" "${ipv6_addr}"
|
||||
if [ $? -eq 0 ]; then
|
||||
SSL_HOST="${server_ip}"
|
||||
echo -e "${green}✓ Self-signed SSL configured successfully${plain}"
|
||||
echo -e "${green}✓ Let's Encrypt IP certificate configured successfully${plain}"
|
||||
else
|
||||
echo -e "${red}✗ Self-signed SSL setup failed${plain}"
|
||||
echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}"
|
||||
SSL_HOST="${server_ip}"
|
||||
fi
|
||||
# Start panel after SSL is configured
|
||||
|
||||
# Restart panel after SSL is configured (restart applies new cert settings)
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service x-ui start >/dev/null 2>&1
|
||||
rc-service x-ui restart >/dev/null 2>&1
|
||||
else
|
||||
systemctl start x-ui >/dev/null 2>&1
|
||||
systemctl restart x-ui >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
;;
|
||||
0)
|
||||
echo -e "${yellow}Skipping SSL setup${plain}"
|
||||
SSL_HOST="${server_ip}"
|
||||
3)
|
||||
# User chose Custom Paths (User Provided) option
|
||||
echo -e "${green}Using custom existing certificate...${plain}"
|
||||
local custom_cert=""
|
||||
local custom_key=""
|
||||
local custom_domain=""
|
||||
|
||||
# 3.1 Request Domain to compose Panel URL later
|
||||
read -rp "Please enter domain name certificate issued for: " custom_domain
|
||||
custom_domain="${custom_domain// /}" # Убираем пробелы
|
||||
|
||||
# 3.2 Loop for Certificate Path
|
||||
while true; do
|
||||
read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert
|
||||
# Strip quotes if present
|
||||
custom_cert=$(echo "$custom_cert" | tr -d '"' | tr -d "'")
|
||||
|
||||
if [[ -f "$custom_cert" && -r "$custom_cert" && -s "$custom_cert" ]]; then
|
||||
break
|
||||
elif [[ ! -f "$custom_cert" ]]; then
|
||||
echo -e "${red}Error: File does not exist! Try again.${plain}"
|
||||
elif [[ ! -r "$custom_cert" ]]; then
|
||||
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
|
||||
else
|
||||
echo -e "${red}Error: File is empty!${plain}"
|
||||
fi
|
||||
done
|
||||
|
||||
# 3.3 Loop for Private Key Path
|
||||
while true; do
|
||||
read -rp "Input private key path (keywords: .key / privatekey): " custom_key
|
||||
# Strip quotes if present
|
||||
custom_key=$(echo "$custom_key" | tr -d '"' | tr -d "'")
|
||||
|
||||
if [[ -f "$custom_key" && -r "$custom_key" && -s "$custom_key" ]]; then
|
||||
break
|
||||
elif [[ ! -f "$custom_key" ]]; then
|
||||
echo -e "${red}Error: File does not exist! Try again.${plain}"
|
||||
elif [[ ! -r "$custom_key" ]]; then
|
||||
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
|
||||
else
|
||||
echo -e "${red}Error: File is empty!${plain}"
|
||||
fi
|
||||
done
|
||||
|
||||
# 3.4 Apply Settings via x-ui binary
|
||||
${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" >/dev/null 2>&1
|
||||
|
||||
# Set SSL_HOST for composing Panel URL
|
||||
if [[ -n "$custom_domain" ]]; then
|
||||
SSL_HOST="$custom_domain"
|
||||
else
|
||||
SSL_HOST="${server_ip}"
|
||||
fi
|
||||
|
||||
echo -e "${green}✓ Custom certificate paths applied.${plain}"
|
||||
echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}"
|
||||
|
||||
systemctl restart x-ui >/dev/null 2>&1 || rc-service x-ui restart >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
|
||||
|
|
@ -501,8 +687,11 @@ config_after_update() {
|
|||
)
|
||||
local server_ip=""
|
||||
for ip_address in "${URL_lists[@]}"; do
|
||||
server_ip=$(${curl_bin} -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
|
||||
if [[ -n "${server_ip}" ]]; then
|
||||
local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null)
|
||||
local http_code=$(echo "$response" | tail -n1)
|
||||
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
|
||||
if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
|
||||
server_ip="${ip_result}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
|
@ -523,7 +712,7 @@ config_after_update() {
|
|||
echo -e "${red} ⚠ NO SSL CERTIFICATE DETECTED ⚠ ${plain}"
|
||||
echo -e "${red}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${yellow}For security, SSL certificate is MANDATORY for all panels.${plain}"
|
||||
echo -e "${yellow}Let's Encrypt requires a domain name; IP certs are not issued. Use self-signed for IP.${plain}"
|
||||
echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}"
|
||||
echo ""
|
||||
|
||||
if [[ -z "${server_ip}" ]]; then
|
||||
|
|
@ -532,7 +721,7 @@ config_after_update() {
|
|||
return
|
||||
fi
|
||||
|
||||
# Prompt and setup SSL (domain or self-signed)
|
||||
# Prompt and setup SSL (domain or IP)
|
||||
prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}"
|
||||
|
||||
echo ""
|
||||
|
|
@ -576,10 +765,10 @@ update_x-ui() {
|
|||
fi
|
||||
fi
|
||||
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
|
||||
${curl_bin} -fLRo ${xui_folder}-linux-$(arch).tar.gz -z ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null
|
||||
${curl_bin} -fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
|
||||
${curl_bin} -4fLRo ${xui_folder}-linux-$(arch).tar.gz -z ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null
|
||||
${curl_bin} -4fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null
|
||||
if [[ $? -ne 0 ]]; then
|
||||
_fail "ERROR: Failed to download x-ui, please be sure that your server can access GitHub"
|
||||
fi
|
||||
|
|
@ -613,6 +802,7 @@ update_x-ui() {
|
|||
rm ${xui_folder} -f >/dev/null 2>&1
|
||||
rm ${xui_folder}/x-ui.service -f >/dev/null 2>&1
|
||||
rm ${xui_folder}/x-ui.service.debian -f >/dev/null 2>&1
|
||||
rm ${xui_folder}/x-ui.service.arch -f >/dev/null 2>&1
|
||||
rm ${xui_folder}/x-ui.service.rhel -f >/dev/null 2>&1
|
||||
rm ${xui_folder}/x-ui -f >/dev/null 2>&1
|
||||
rm ${xui_folder}/x-ui.sh -f >/dev/null 2>&1
|
||||
|
|
@ -695,6 +885,15 @@ update_x-ui() {
|
|||
fi
|
||||
fi
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
if [ -f "x-ui.service.arch" ]; then
|
||||
echo -e "${green}Installing arch-like systemd unit...${plain}"
|
||||
cp -f x-ui.service.arch ${xui_service}/x-ui.service >/dev/null 2>&1
|
||||
if [[ $? -eq 0 ]]; then
|
||||
service_installed=true
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if [ -f "x-ui.service.rhel" ]; then
|
||||
echo -e "${green}Installing rhel-like systemd unit...${plain}"
|
||||
|
|
@ -713,6 +912,9 @@ update_x-ui() {
|
|||
ubuntu | debian | armbian)
|
||||
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1
|
||||
;;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,5 @@ func HashPasswordAsBcrypt(password string) (string, error) {
|
|||
|
||||
// CheckPasswordHash verifies if the given password matches the bcrypt hash.
|
||||
func CheckPasswordHash(hash, password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,13 +24,22 @@ type Config struct {
|
|||
// FetchVlessFlags returns map[email]enabled
|
||||
func FetchVlessFlags(cfg Config) (map[string]bool, error) {
|
||||
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
||||
var conn *ldap.Conn
|
||||
var err error
|
||||
|
||||
scheme := "ldap"
|
||||
if cfg.UseTLS {
|
||||
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false})
|
||||
} else {
|
||||
conn, err = ldap.Dial("tcp", addr)
|
||||
scheme = "ldaps"
|
||||
}
|
||||
|
||||
ldapURL := fmt.Sprintf("%s://%s", scheme, addr)
|
||||
|
||||
var opts []ldap.DialOpt
|
||||
if cfg.UseTLS {
|
||||
opts = append(opts, ldap.DialWithTLSConfig(&tls.Config{
|
||||
InsecureSkipVerify: false,
|
||||
}))
|
||||
}
|
||||
|
||||
conn, err := ldap.DialURL(ldapURL, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -91,13 +100,22 @@ func FetchVlessFlags(cfg Config) (map[string]bool, error) {
|
|||
// AuthenticateUser searches user by cfg.UserAttr and attempts to bind with provided password.
|
||||
func AuthenticateUser(cfg Config, username, password string) (bool, error) {
|
||||
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
||||
var conn *ldap.Conn
|
||||
var err error
|
||||
|
||||
scheme := "ldap"
|
||||
if cfg.UseTLS {
|
||||
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false})
|
||||
} else {
|
||||
conn, err = ldap.Dial("tcp", addr)
|
||||
scheme = "ldaps"
|
||||
}
|
||||
|
||||
ldapURL := fmt.Sprintf("%s://%s", scheme, addr)
|
||||
|
||||
var opts []ldap.DialOpt
|
||||
if cfg.UseTLS {
|
||||
opts = append(opts, ldap.DialWithTLSConfig(&tls.Config{
|
||||
InsecureSkipVerify: false,
|
||||
}))
|
||||
}
|
||||
|
||||
conn, err := ldap.DialURL(ldapURL, opts...)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ var (
|
|||
// init initializes the character sequences used for random string generation.
|
||||
// It sets up arrays for numbers, lowercase letters, uppercase letters, and combinations.
|
||||
func init() {
|
||||
for i := 0; i < 10; i++ {
|
||||
for i := range 10 {
|
||||
numSeq[i] = rune('0' + i)
|
||||
}
|
||||
for i := 0; i < 26; i++ {
|
||||
for i := range 26 {
|
||||
lowerSeq[i] = rune('a' + i)
|
||||
upperSeq[i] = rune('A' + i)
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ func init() {
|
|||
// Seq generates a random string of length n containing alphanumeric characters (numbers, lowercase and uppercase letters).
|
||||
func Seq(n int) string {
|
||||
runes := make([]rune, n)
|
||||
for i := 0; i < n; i++ {
|
||||
for i := range n {
|
||||
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(allSeq))))
|
||||
if err != nil {
|
||||
panic("crypto/rand failed: " + err.Error())
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import "reflect"
|
|||
func GetFields(t reflect.Type) []reflect.StructField {
|
||||
num := t.NumField()
|
||||
fields := make([]reflect.StructField, 0, num)
|
||||
for i := 0; i < num; i++ {
|
||||
for i := range num {
|
||||
fields = append(fields, t.Field(i))
|
||||
}
|
||||
return fields
|
||||
|
|
@ -17,7 +17,7 @@ func GetFields(t reflect.Type) []reflect.StructField {
|
|||
func GetFieldValues(v reflect.Value) []reflect.Value {
|
||||
num := v.NumField()
|
||||
fields := make([]reflect.Value, 0, num)
|
||||
for i := 0; i < num; i++ {
|
||||
for i := range num {
|
||||
fields = append(fields, v.Field(i))
|
||||
}
|
||||
return fields
|
||||
|
|
|
|||
|
|
@ -47,11 +47,11 @@ func CPUPercentRaw() (float64, error) {
|
|||
var out [5]uint64
|
||||
switch len(raw) {
|
||||
case 5 * 8:
|
||||
for i := 0; i < 5; i++ {
|
||||
for i := range 5 {
|
||||
out[i] = binary.LittleEndian.Uint64(raw[i*8 : (i+1)*8])
|
||||
}
|
||||
case 5 * 4:
|
||||
for i := 0; i < 5; i++ {
|
||||
for i := range 5 {
|
||||
out[i] = uint64(binary.LittleEndian.Uint32(raw[i*4 : (i+1)*4]))
|
||||
}
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ const Protocols = {
|
|||
MIXED: 'mixed',
|
||||
HTTP: 'http',
|
||||
WIREGUARD: 'wireguard',
|
||||
TUN: 'tun',
|
||||
};
|
||||
|
||||
const SSMethods = {
|
||||
|
|
@ -318,14 +319,12 @@ TcpStreamSettings.TcpResponse = class extends XrayCommonClass {
|
|||
class KcpStreamSettings extends XrayCommonClass {
|
||||
constructor(
|
||||
mtu = 1350,
|
||||
tti = 50,
|
||||
tti = 20,
|
||||
uplinkCapacity = 5,
|
||||
downlinkCapacity = 20,
|
||||
congestion = false,
|
||||
readBufferSize = 2,
|
||||
writeBufferSize = 2,
|
||||
type = 'none',
|
||||
seed = RandomUtil.randomSeq(10),
|
||||
readBufferSize = 1,
|
||||
writeBufferSize = 1,
|
||||
) {
|
||||
super();
|
||||
this.mtu = mtu;
|
||||
|
|
@ -335,8 +334,6 @@ class KcpStreamSettings extends XrayCommonClass {
|
|||
this.congestion = congestion;
|
||||
this.readBuffer = readBufferSize;
|
||||
this.writeBuffer = writeBufferSize;
|
||||
this.type = type;
|
||||
this.seed = seed;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
|
|
@ -348,8 +345,6 @@ class KcpStreamSettings extends XrayCommonClass {
|
|||
json.congestion,
|
||||
json.readBufferSize,
|
||||
json.writeBufferSize,
|
||||
ObjectUtil.isEmpty(json.header) ? 'none' : json.header.type,
|
||||
json.seed,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -362,10 +357,6 @@ class KcpStreamSettings extends XrayCommonClass {
|
|||
congestion: this.congestion,
|
||||
readBufferSize: this.readBuffer,
|
||||
writeBufferSize: this.writeBuffer,
|
||||
header: {
|
||||
type: this.type,
|
||||
},
|
||||
seed: this.seed,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -496,6 +487,19 @@ class xHTTPStreamSettings extends XrayCommonClass {
|
|||
noSSEHeader = false,
|
||||
xPaddingBytes = "100-1000",
|
||||
mode = MODE_OPTION.AUTO,
|
||||
xPaddingObfsMode = false,
|
||||
xPaddingKey = '',
|
||||
xPaddingHeader = '',
|
||||
xPaddingPlacement = '',
|
||||
xPaddingMethod = '',
|
||||
uplinkHTTPMethod = '',
|
||||
sessionPlacement = '',
|
||||
sessionKey = '',
|
||||
seqPlacement = '',
|
||||
seqKey = '',
|
||||
uplinkDataPlacement = '',
|
||||
uplinkDataKey = '',
|
||||
uplinkChunkSize = 0,
|
||||
) {
|
||||
super();
|
||||
this.path = path;
|
||||
|
|
@ -507,6 +511,19 @@ class xHTTPStreamSettings extends XrayCommonClass {
|
|||
this.noSSEHeader = noSSEHeader;
|
||||
this.xPaddingBytes = xPaddingBytes;
|
||||
this.mode = mode;
|
||||
this.xPaddingObfsMode = xPaddingObfsMode;
|
||||
this.xPaddingKey = xPaddingKey;
|
||||
this.xPaddingHeader = xPaddingHeader;
|
||||
this.xPaddingPlacement = xPaddingPlacement;
|
||||
this.xPaddingMethod = xPaddingMethod;
|
||||
this.uplinkHTTPMethod = uplinkHTTPMethod;
|
||||
this.sessionPlacement = sessionPlacement;
|
||||
this.sessionKey = sessionKey;
|
||||
this.seqPlacement = seqPlacement;
|
||||
this.seqKey = seqKey;
|
||||
this.uplinkDataPlacement = uplinkDataPlacement;
|
||||
this.uplinkDataKey = uplinkDataKey;
|
||||
this.uplinkChunkSize = uplinkChunkSize;
|
||||
}
|
||||
|
||||
addHeader(name, value) {
|
||||
|
|
@ -528,6 +545,19 @@ class xHTTPStreamSettings extends XrayCommonClass {
|
|||
json.noSSEHeader,
|
||||
json.xPaddingBytes,
|
||||
json.mode,
|
||||
json.xPaddingObfsMode,
|
||||
json.xPaddingKey,
|
||||
json.xPaddingHeader,
|
||||
json.xPaddingPlacement,
|
||||
json.xPaddingMethod,
|
||||
json.uplinkHTTPMethod,
|
||||
json.sessionPlacement,
|
||||
json.sessionKey,
|
||||
json.seqPlacement,
|
||||
json.seqKey,
|
||||
json.uplinkDataPlacement,
|
||||
json.uplinkDataKey,
|
||||
json.uplinkChunkSize,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -542,6 +572,19 @@ class xHTTPStreamSettings extends XrayCommonClass {
|
|||
noSSEHeader: this.noSSEHeader,
|
||||
xPaddingBytes: this.xPaddingBytes,
|
||||
mode: this.mode,
|
||||
xPaddingObfsMode: this.xPaddingObfsMode,
|
||||
xPaddingKey: this.xPaddingKey,
|
||||
xPaddingHeader: this.xPaddingHeader,
|
||||
xPaddingPlacement: this.xPaddingPlacement,
|
||||
xPaddingMethod: this.xPaddingMethod,
|
||||
uplinkHTTPMethod: this.uplinkHTTPMethod,
|
||||
sessionPlacement: this.sessionPlacement,
|
||||
sessionKey: this.sessionKey,
|
||||
seqPlacement: this.seqPlacement,
|
||||
seqKey: this.seqKey,
|
||||
uplinkDataPlacement: this.uplinkDataPlacement,
|
||||
uplinkDataKey: this.uplinkDataKey,
|
||||
uplinkChunkSize: this.uplinkChunkSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -553,7 +596,6 @@ class TlsStreamSettings extends XrayCommonClass {
|
|||
maxVersion = TLS_VERSION_OPTION.TLS13,
|
||||
cipherSuites = '',
|
||||
rejectUnknownSni = false,
|
||||
verifyPeerCertInNames = ['dns.google', 'cloudflare-dns.com'],
|
||||
disableSystemRoot = false,
|
||||
enableSessionResumption = false,
|
||||
certificates = [new TlsStreamSettings.Cert()],
|
||||
|
|
@ -568,7 +610,6 @@ class TlsStreamSettings extends XrayCommonClass {
|
|||
this.maxVersion = maxVersion;
|
||||
this.cipherSuites = cipherSuites;
|
||||
this.rejectUnknownSni = rejectUnknownSni;
|
||||
this.verifyPeerCertInNames = Array.isArray(verifyPeerCertInNames) ? verifyPeerCertInNames.join(",") : verifyPeerCertInNames;
|
||||
this.disableSystemRoot = disableSystemRoot;
|
||||
this.enableSessionResumption = enableSessionResumption;
|
||||
this.certs = certificates;
|
||||
|
|
@ -594,7 +635,7 @@ class TlsStreamSettings extends XrayCommonClass {
|
|||
}
|
||||
|
||||
if (!ObjectUtil.isEmpty(json.settings)) {
|
||||
settings = new TlsStreamSettings.Settings(json.settings.allowInsecure, json.settings.fingerprint, json.settings.echConfigList);
|
||||
settings = new TlsStreamSettings.Settings(json.settings.fingerprint, json.settings.echConfigList);
|
||||
}
|
||||
return new TlsStreamSettings(
|
||||
json.serverName,
|
||||
|
|
@ -602,7 +643,6 @@ class TlsStreamSettings extends XrayCommonClass {
|
|||
json.maxVersion,
|
||||
json.cipherSuites,
|
||||
json.rejectUnknownSni,
|
||||
json.verifyPeerCertInNames,
|
||||
json.disableSystemRoot,
|
||||
json.enableSessionResumption,
|
||||
certs,
|
||||
|
|
@ -620,7 +660,6 @@ class TlsStreamSettings extends XrayCommonClass {
|
|||
maxVersion: this.maxVersion,
|
||||
cipherSuites: this.cipherSuites,
|
||||
rejectUnknownSni: this.rejectUnknownSni,
|
||||
verifyPeerCertInNames: this.verifyPeerCertInNames.split(","),
|
||||
disableSystemRoot: this.disableSystemRoot,
|
||||
enableSessionResumption: this.enableSessionResumption,
|
||||
certificates: TlsStreamSettings.toJsonArray(this.certs),
|
||||
|
|
@ -699,25 +738,21 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
|||
|
||||
TlsStreamSettings.Settings = class extends XrayCommonClass {
|
||||
constructor(
|
||||
allowInsecure = false,
|
||||
fingerprint = UTLS_FINGERPRINT.UTLS_CHROME,
|
||||
echConfigList = '',
|
||||
) {
|
||||
super();
|
||||
this.allowInsecure = allowInsecure;
|
||||
this.fingerprint = fingerprint;
|
||||
this.echConfigList = echConfigList;
|
||||
}
|
||||
static fromJson(json = {}) {
|
||||
return new TlsStreamSettings.Settings(
|
||||
json.allowInsecure,
|
||||
json.fingerprint,
|
||||
json.echConfigList,
|
||||
);
|
||||
}
|
||||
toJson() {
|
||||
return {
|
||||
allowInsecure: this.allowInsecure,
|
||||
fingerprint: this.fingerprint,
|
||||
echConfigList: this.echConfigList
|
||||
};
|
||||
|
|
@ -928,6 +963,68 @@ 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 {
|
||||
constructor(network = 'tcp',
|
||||
security = 'none',
|
||||
|
|
@ -940,6 +1037,7 @@ class StreamSettings extends XrayCommonClass {
|
|||
grpcSettings = new GrpcStreamSettings(),
|
||||
httpupgradeSettings = new HTTPUpgradeStreamSettings(),
|
||||
xhttpSettings = new xHTTPStreamSettings(),
|
||||
finalmask = new FinalMaskStreamSettings(),
|
||||
sockopt = undefined,
|
||||
) {
|
||||
super();
|
||||
|
|
@ -954,9 +1052,24 @@ class StreamSettings extends XrayCommonClass {
|
|||
this.grpc = grpcSettings;
|
||||
this.httpupgrade = httpupgradeSettings;
|
||||
this.xhttp = xhttpSettings;
|
||||
this.finalmask = finalmask;
|
||||
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() {
|
||||
return this.security === "tls";
|
||||
}
|
||||
|
|
@ -1003,6 +1116,7 @@ class StreamSettings extends XrayCommonClass {
|
|||
GrpcStreamSettings.fromJson(json.grpcSettings),
|
||||
HTTPUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
|
||||
xHTTPStreamSettings.fromJson(json.xhttpSettings),
|
||||
FinalMaskStreamSettings.fromJson(json.finalmask),
|
||||
SockoptStreamSettings.fromJson(json.sockopt),
|
||||
);
|
||||
}
|
||||
|
|
@ -1021,6 +1135,7 @@ class StreamSettings extends XrayCommonClass {
|
|||
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
|
||||
httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,
|
||||
xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined,
|
||||
finalmask: this.hasFinalMask ? this.finalmask.toJson() : undefined,
|
||||
sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
|
||||
};
|
||||
}
|
||||
|
|
@ -1191,14 +1306,6 @@ class Inbound extends XrayCommonClass {
|
|||
return null;
|
||||
}
|
||||
|
||||
get kcpType() {
|
||||
return this.stream.kcp.type;
|
||||
}
|
||||
|
||||
get kcpSeed() {
|
||||
return this.stream.kcp.seed;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return this.stream.grpc.serviceName;
|
||||
}
|
||||
|
|
@ -1275,8 +1382,6 @@ class Inbound extends XrayCommonClass {
|
|||
}
|
||||
} else if (network === 'kcp') {
|
||||
const kcp = this.stream.kcp;
|
||||
obj.type = kcp.type;
|
||||
obj.path = kcp.seed;
|
||||
} else if (network === 'ws') {
|
||||
const ws = this.stream.ws;
|
||||
obj.path = ws.path;
|
||||
|
|
@ -1308,9 +1413,6 @@ class Inbound extends XrayCommonClass {
|
|||
if (this.stream.tls.alpn.length > 0) {
|
||||
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));
|
||||
|
|
@ -1339,8 +1441,6 @@ class Inbound extends XrayCommonClass {
|
|||
break;
|
||||
case "kcp":
|
||||
const kcp = this.stream.kcp;
|
||||
params.set("headerType", kcp.type);
|
||||
params.set("seed", kcp.seed);
|
||||
break;
|
||||
case "ws":
|
||||
const ws = this.stream.ws;
|
||||
|
|
@ -1373,9 +1473,6 @@ class Inbound extends XrayCommonClass {
|
|||
if (this.stream.isTls) {
|
||||
params.set("fp", this.stream.tls.settings.fingerprint);
|
||||
params.set("alpn", this.stream.tls.alpn);
|
||||
if (this.stream.tls.settings.allowInsecure) {
|
||||
params.set("allowInsecure", "1");
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
|
||||
params.set("sni", this.stream.tls.sni);
|
||||
}
|
||||
|
|
@ -1444,8 +1541,6 @@ class Inbound extends XrayCommonClass {
|
|||
break;
|
||||
case "kcp":
|
||||
const kcp = this.stream.kcp;
|
||||
params.set("headerType", kcp.type);
|
||||
params.set("seed", kcp.seed);
|
||||
break;
|
||||
case "ws":
|
||||
const ws = this.stream.ws;
|
||||
|
|
@ -1478,9 +1573,6 @@ class Inbound extends XrayCommonClass {
|
|||
if (this.stream.isTls) {
|
||||
params.set("fp", this.stream.tls.settings.fingerprint);
|
||||
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) {
|
||||
params.set("ech", this.stream.tls.settings.echConfigList);
|
||||
}
|
||||
|
|
@ -1525,8 +1617,6 @@ class Inbound extends XrayCommonClass {
|
|||
break;
|
||||
case "kcp":
|
||||
const kcp = this.stream.kcp;
|
||||
params.set("headerType", kcp.type);
|
||||
params.set("seed", kcp.seed);
|
||||
break;
|
||||
case "ws":
|
||||
const ws = this.stream.ws;
|
||||
|
|
@ -1559,9 +1649,6 @@ class Inbound extends XrayCommonClass {
|
|||
if (this.stream.isTls) {
|
||||
params.set("fp", this.stream.tls.settings.fingerprint);
|
||||
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) {
|
||||
params.set("ech", this.stream.tls.settings.echConfigList);
|
||||
}
|
||||
|
|
@ -1739,6 +1826,7 @@ Inbound.Settings = class extends XrayCommonClass {
|
|||
case Protocols.MIXED: return new Inbound.MixedSettings(protocol);
|
||||
case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
|
||||
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
|
||||
case Protocols.TUN: return new Inbound.TunSettings(protocol);
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1753,6 +1841,7 @@ Inbound.Settings = class extends XrayCommonClass {
|
|||
case Protocols.MIXED: return Inbound.MixedSettings.fromJson(json);
|
||||
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
|
||||
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
|
||||
case Protocols.TUN: return Inbound.TunSettings.fromJson(json);
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1944,7 +2033,9 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
|||
json.selectedAuth = this.selectedAuth;
|
||||
}
|
||||
|
||||
if (this.testseed && this.testseed.length >= 4) {
|
||||
// Only include testseed if at least one client has a flow set
|
||||
const hasFlow = this.vlesses && this.vlesses.some(vless => vless.flow && vless.flow !== '');
|
||||
if (hasFlow && this.testseed && this.testseed.length >= 4) {
|
||||
json.testseed = this.testseed;
|
||||
}
|
||||
|
||||
|
|
@ -2586,3 +2677,34 @@ Inbound.WireguardSettings.Peer = class extends XrayCommonClass {
|
|||
};
|
||||
}
|
||||
};
|
||||
|
||||
Inbound.TunSettings = class extends Inbound.Settings {
|
||||
constructor(
|
||||
protocol,
|
||||
name = 'xray0',
|
||||
mtu = 1500,
|
||||
userLevel = 0
|
||||
) {
|
||||
super(protocol);
|
||||
this.name = name;
|
||||
this.mtu = mtu;
|
||||
this.userLevel = userLevel;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
return new Inbound.TunSettings(
|
||||
Protocols.TUN,
|
||||
json.name ?? 'xray0',
|
||||
json.mtu ?? json.MTU ?? 1500,
|
||||
json.userLevel ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
name: this.name || 'xray0',
|
||||
mtu: this.mtu || 1500,
|
||||
userLevel: this.userLevel || 0,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ const Protocols = {
|
|||
Shadowsocks: "shadowsocks",
|
||||
Socks: "socks",
|
||||
HTTP: "http",
|
||||
Wireguard: "wireguard"
|
||||
Wireguard: "wireguard",
|
||||
Hysteria: "hysteria"
|
||||
};
|
||||
|
||||
const SSMethods = {
|
||||
|
|
@ -165,14 +166,12 @@ class TcpStreamSettings extends CommonClass {
|
|||
class KcpStreamSettings extends CommonClass {
|
||||
constructor(
|
||||
mtu = 1350,
|
||||
tti = 50,
|
||||
tti = 20,
|
||||
uplinkCapacity = 5,
|
||||
downlinkCapacity = 20,
|
||||
congestion = false,
|
||||
readBufferSize = 2,
|
||||
writeBufferSize = 2,
|
||||
type = 'none',
|
||||
seed = '',
|
||||
readBufferSize = 1,
|
||||
writeBufferSize = 1,
|
||||
) {
|
||||
super();
|
||||
this.mtu = mtu;
|
||||
|
|
@ -182,8 +181,6 @@ class KcpStreamSettings extends CommonClass {
|
|||
this.congestion = congestion;
|
||||
this.readBuffer = readBufferSize;
|
||||
this.writeBuffer = writeBufferSize;
|
||||
this.type = type;
|
||||
this.seed = seed;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
|
|
@ -195,8 +192,6 @@ class KcpStreamSettings extends CommonClass {
|
|||
json.congestion,
|
||||
json.readBufferSize,
|
||||
json.writeBufferSize,
|
||||
ObjectUtil.isEmpty(json.header) ? 'none' : json.header.type,
|
||||
json.seed,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -209,10 +204,6 @@ class KcpStreamSettings extends CommonClass {
|
|||
congestion: this.congestion,
|
||||
readBufferSize: this.readBuffer,
|
||||
writeBufferSize: this.writeBuffer,
|
||||
header: {
|
||||
type: this.type,
|
||||
},
|
||||
seed: this.seed,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -354,15 +345,17 @@ class TlsStreamSettings extends CommonClass {
|
|||
serverName = '',
|
||||
alpn = [],
|
||||
fingerprint = '',
|
||||
allowInsecure = false,
|
||||
echConfigList = '',
|
||||
verifyPeerCertByName = '',
|
||||
pinnedPeerCertSha256 = '',
|
||||
) {
|
||||
super();
|
||||
this.serverName = serverName;
|
||||
this.alpn = alpn;
|
||||
this.fingerprint = fingerprint;
|
||||
this.allowInsecure = allowInsecure;
|
||||
this.echConfigList = echConfigList;
|
||||
this.verifyPeerCertByName = verifyPeerCertByName;
|
||||
this.pinnedPeerCertSha256 = pinnedPeerCertSha256;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
|
|
@ -370,8 +363,9 @@ class TlsStreamSettings extends CommonClass {
|
|||
json.serverName,
|
||||
json.alpn,
|
||||
json.fingerprint,
|
||||
json.allowInsecure,
|
||||
json.echConfigList,
|
||||
json.verifyPeerCertByName,
|
||||
json.pinnedPeerCertSha256,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -380,8 +374,9 @@ class TlsStreamSettings extends CommonClass {
|
|||
serverName: this.serverName,
|
||||
alpn: this.alpn,
|
||||
fingerprint: this.fingerprint,
|
||||
allowInsecure: this.allowInsecure,
|
||||
echConfigList: this.echConfigList
|
||||
echConfigList: this.echConfigList,
|
||||
verifyPeerCertByName: this.verifyPeerCertByName,
|
||||
pinnedPeerCertSha256: this.pinnedPeerCertSha256
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -424,6 +419,102 @@ class RealityStreamSettings extends CommonClass {
|
|||
};
|
||||
}
|
||||
};
|
||||
|
||||
class HysteriaStreamSettings extends CommonClass {
|
||||
constructor(
|
||||
version = 2,
|
||||
auth = '',
|
||||
congestion = '',
|
||||
up = '0',
|
||||
down = '0',
|
||||
udphopPort = '',
|
||||
udphopIntervalMin = 30,
|
||||
udphopIntervalMax = 30,
|
||||
initStreamReceiveWindow = 8388608,
|
||||
maxStreamReceiveWindow = 8388608,
|
||||
initConnectionReceiveWindow = 20971520,
|
||||
maxConnectionReceiveWindow = 20971520,
|
||||
maxIdleTimeout = 30,
|
||||
keepAlivePeriod = 0,
|
||||
disablePathMTUDiscovery = false
|
||||
) {
|
||||
super();
|
||||
this.version = version;
|
||||
this.auth = auth;
|
||||
this.congestion = congestion;
|
||||
this.up = up;
|
||||
this.down = down;
|
||||
this.udphopPort = udphopPort;
|
||||
this.udphopIntervalMin = udphopIntervalMin;
|
||||
this.udphopIntervalMax = udphopIntervalMax;
|
||||
this.initStreamReceiveWindow = initStreamReceiveWindow;
|
||||
this.maxStreamReceiveWindow = maxStreamReceiveWindow;
|
||||
this.initConnectionReceiveWindow = initConnectionReceiveWindow;
|
||||
this.maxConnectionReceiveWindow = maxConnectionReceiveWindow;
|
||||
this.maxIdleTimeout = maxIdleTimeout;
|
||||
this.keepAlivePeriod = keepAlivePeriod;
|
||||
this.disablePathMTUDiscovery = disablePathMTUDiscovery;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
let udphopPort = '';
|
||||
let udphopIntervalMin = 30;
|
||||
let udphopIntervalMax = 30;
|
||||
if (json.udphop) {
|
||||
udphopPort = json.udphop.port || '';
|
||||
// Backward compatibility: if old 'interval' exists, use it for both min/max
|
||||
if (json.udphop.interval !== undefined) {
|
||||
udphopIntervalMin = json.udphop.interval;
|
||||
udphopIntervalMax = json.udphop.interval;
|
||||
} else {
|
||||
udphopIntervalMin = json.udphop.intervalMin || 30;
|
||||
udphopIntervalMax = json.udphop.intervalMax || 30;
|
||||
}
|
||||
}
|
||||
return new HysteriaStreamSettings(
|
||||
json.version,
|
||||
json.auth,
|
||||
json.congestion,
|
||||
json.up,
|
||||
json.down,
|
||||
udphopPort,
|
||||
udphopIntervalMin,
|
||||
udphopIntervalMax,
|
||||
json.initStreamReceiveWindow,
|
||||
json.maxStreamReceiveWindow,
|
||||
json.initConnectionReceiveWindow,
|
||||
json.maxConnectionReceiveWindow,
|
||||
json.maxIdleTimeout,
|
||||
json.keepAlivePeriod,
|
||||
json.disablePathMTUDiscovery
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
const result = {
|
||||
version: this.version,
|
||||
auth: this.auth,
|
||||
congestion: this.congestion,
|
||||
up: this.up,
|
||||
down: this.down,
|
||||
initStreamReceiveWindow: this.initStreamReceiveWindow,
|
||||
maxStreamReceiveWindow: this.maxStreamReceiveWindow,
|
||||
initConnectionReceiveWindow: this.initConnectionReceiveWindow,
|
||||
maxConnectionReceiveWindow: this.maxConnectionReceiveWindow,
|
||||
maxIdleTimeout: this.maxIdleTimeout,
|
||||
keepAlivePeriod: this.keepAlivePeriod,
|
||||
disablePathMTUDiscovery: this.disablePathMTUDiscovery
|
||||
};
|
||||
if (this.udphopPort) {
|
||||
result.udphop = {
|
||||
port: this.udphopPort,
|
||||
intervalMin: this.udphopIntervalMin,
|
||||
intervalMax: this.udphopIntervalMax
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
class SockoptStreamSettings extends CommonClass {
|
||||
constructor(
|
||||
dialerProxy = "",
|
||||
|
|
@ -473,6 +564,66 @@ 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 {
|
||||
constructor(
|
||||
network = 'tcp',
|
||||
|
|
@ -485,6 +636,8 @@ class StreamSettings extends CommonClass {
|
|||
grpcSettings = new GrpcStreamSettings(),
|
||||
httpupgradeSettings = new HttpUpgradeStreamSettings(),
|
||||
xhttpSettings = new xHTTPStreamSettings(),
|
||||
hysteriaSettings = new HysteriaStreamSettings(),
|
||||
finalmask = new FinalMaskStreamSettings(),
|
||||
sockopt = undefined,
|
||||
) {
|
||||
super();
|
||||
|
|
@ -498,9 +651,25 @@ class StreamSettings extends CommonClass {
|
|||
this.grpc = grpcSettings;
|
||||
this.httpupgrade = httpupgradeSettings;
|
||||
this.xhttp = xhttpSettings;
|
||||
this.hysteria = hysteriaSettings;
|
||||
this.finalmask = finalmask;
|
||||
this.sockopt = sockopt;
|
||||
}
|
||||
|
||||
addUdpMask(type = 'salamander') {
|
||||
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() {
|
||||
return this.security === 'tls';
|
||||
}
|
||||
|
|
@ -529,6 +698,8 @@ class StreamSettings extends CommonClass {
|
|||
GrpcStreamSettings.fromJson(json.grpcSettings),
|
||||
HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
|
||||
xHTTPStreamSettings.fromJson(json.xhttpSettings),
|
||||
HysteriaStreamSettings.fromJson(json.hysteriaSettings),
|
||||
FinalMaskStreamSettings.fromJson(json.finalmask),
|
||||
SockoptStreamSettings.fromJson(json.sockopt),
|
||||
);
|
||||
}
|
||||
|
|
@ -546,6 +717,8 @@ class StreamSettings extends CommonClass {
|
|||
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
|
||||
httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,
|
||||
xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined,
|
||||
hysteriaSettings: network === 'hysteria' ? this.hysteria.toJson() : undefined,
|
||||
finalmask: this.hasFinalMask ? this.finalmask.toJson() : undefined,
|
||||
sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
|
||||
};
|
||||
}
|
||||
|
|
@ -609,7 +782,8 @@ class Outbound extends CommonClass {
|
|||
}
|
||||
|
||||
canEnableTls() {
|
||||
if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol)) return false;
|
||||
if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks, Protocols.Hysteria].includes(this.protocol)) return false;
|
||||
if (this.protocol === Protocols.Hysteria) return this.stream.network === 'hysteria';
|
||||
return ["tcp", "ws", "http", "grpc", "httpupgrade", "xhttp"].includes(this.stream.network);
|
||||
}
|
||||
|
||||
|
|
@ -634,7 +808,7 @@ class Outbound extends CommonClass {
|
|||
}
|
||||
|
||||
canEnableStream() {
|
||||
return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol);
|
||||
return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks, Protocols.Hysteria].includes(this.protocol);
|
||||
}
|
||||
|
||||
canEnableMux() {
|
||||
|
|
@ -673,7 +847,8 @@ class Outbound extends CommonClass {
|
|||
Protocols.Trojan,
|
||||
Protocols.Shadowsocks,
|
||||
Protocols.Socks,
|
||||
Protocols.HTTP
|
||||
Protocols.HTTP,
|
||||
Protocols.Hysteria
|
||||
].includes(this.protocol);
|
||||
}
|
||||
|
||||
|
|
@ -722,6 +897,9 @@ class Outbound extends CommonClass {
|
|||
case Protocols.Trojan:
|
||||
case 'ss':
|
||||
return this.fromParamLink(link);
|
||||
case 'hysteria2':
|
||||
case Protocols.Hysteria:
|
||||
return this.fromHysteriaLink(link);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
@ -754,8 +932,7 @@ class Outbound extends CommonClass {
|
|||
stream.tls = new TlsStreamSettings(
|
||||
json.sni,
|
||||
json.alpn ? json.alpn.split(',') : [],
|
||||
json.fp,
|
||||
json.allowInsecure);
|
||||
json.fp);
|
||||
}
|
||||
|
||||
const port = json.port * 1;
|
||||
|
|
@ -796,10 +973,9 @@ class Outbound extends CommonClass {
|
|||
if (security == 'tls') {
|
||||
let fp = url.searchParams.get('fp') ?? 'none';
|
||||
let alpn = url.searchParams.get('alpn');
|
||||
let allowInsecure = url.searchParams.get('allowInsecure');
|
||||
let sni = url.searchParams.get('sni') ?? '';
|
||||
let ech = url.searchParams.get('ech') ?? '';
|
||||
stream.tls = new TlsStreamSettings(sni, alpn ? alpn.split(',') : [], fp, allowInsecure == 1, ech);
|
||||
stream.tls = new TlsStreamSettings(sni, alpn ? alpn.split(',') : [], fp, ech);
|
||||
}
|
||||
|
||||
if (security == 'reality') {
|
||||
|
|
@ -842,6 +1018,70 @@ class Outbound extends CommonClass {
|
|||
remark = remark.length > 0 ? remark.substring(1) : 'out-' + protocol + '-' + port;
|
||||
return new Outbound(remark, protocol, settings, stream);
|
||||
}
|
||||
|
||||
static fromHysteriaLink(link) {
|
||||
// Parse hysteria2://password@address:port[?param1=value1¶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 {
|
||||
|
|
@ -862,6 +1102,7 @@ Outbound.Settings = class extends CommonClass {
|
|||
case Protocols.Socks: return new Outbound.SocksSettings();
|
||||
case Protocols.HTTP: return new Outbound.HttpSettings();
|
||||
case Protocols.Wireguard: return new Outbound.WireguardSettings();
|
||||
case Protocols.Hysteria: return new Outbound.HysteriaSettings();
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -878,6 +1119,7 @@ Outbound.Settings = class extends CommonClass {
|
|||
case Protocols.Socks: return Outbound.SocksSettings.fromJson(json);
|
||||
case Protocols.HTTP: return Outbound.HttpSettings.fromJson(json);
|
||||
case Protocols.Wireguard: return Outbound.WireguardSettings.fromJson(json);
|
||||
case Protocols.Hysteria: return Outbound.HysteriaSettings.fromJson(json);
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1096,11 +1338,14 @@ Outbound.VLESSSettings = class extends CommonClass {
|
|||
flow: this.flow,
|
||||
encryption: this.encryption,
|
||||
};
|
||||
if (this.testpre > 0) {
|
||||
result.testpre = this.testpre;
|
||||
}
|
||||
if (this.testseed && this.testseed.length >= 4) {
|
||||
result.testseed = this.testseed;
|
||||
// Only include Vision settings when flow is set
|
||||
if (this.flow && this.flow !== '') {
|
||||
if (this.testpre > 0) {
|
||||
result.testpre = this.testpre;
|
||||
}
|
||||
if (this.testseed && this.testseed.length >= 4) {
|
||||
result.testseed = this.testseed;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
@ -1324,4 +1569,30 @@ Outbound.WireguardSettings.Peer = class extends CommonClass {
|
|||
keepAlive: this.keepAlive ?? undefined,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
Outbound.HysteriaSettings = class extends CommonClass {
|
||||
constructor(address = '', port = 443, version = 2) {
|
||||
super();
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
if (Object.keys(json).length === 0) return new Outbound.HysteriaSettings();
|
||||
return new Outbound.HysteriaSettings(
|
||||
json.address,
|
||||
json.port,
|
||||
json.version
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
version: this.version
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
@ -1,18 +1,15 @@
|
|||
// List of popular services for VLESS Reality Target/SNI randomization
|
||||
const REALITY_TARGETS = [
|
||||
{ target: 'www.icloud.com:443', sni: 'www.icloud.com,icloud.com' },
|
||||
{ target: 'www.apple.com:443', sni: 'www.apple.com,apple.com' },
|
||||
{ target: 'www.tesla.com:443', sni: 'www.tesla.com,tesla.com' },
|
||||
{ target: 'www.sony.com:443', sni: 'www.sony.com,sony.com' },
|
||||
{ target: 'www.nvidia.com:443', sni: 'www.nvidia.com,nvidia.com' },
|
||||
{ target: 'www.amd.com:443', sni: 'www.amd.com,amd.com' },
|
||||
{ target: 'azure.microsoft.com:443', sni: 'azure.microsoft.com,www.azure.com' },
|
||||
{ target: 'aws.amazon.com:443', sni: 'aws.amazon.com,amazon.com' },
|
||||
{ target: 'www.bing.com:443', sni: 'www.bing.com,bing.com' },
|
||||
{ target: 'www.oracle.com:443', sni: 'www.oracle.com,oracle.com' },
|
||||
{ target: 'www.intel.com:443', sni: 'www.intel.com,intel.com' },
|
||||
{ target: 'www.microsoft.com:443', sni: 'www.microsoft.com,microsoft.com' },
|
||||
{ target: 'www.amazon.com:443', sni: 'www.amazon.com,amazon.com' }
|
||||
{ target: 'www.apple.com:443', sni: 'www.apple.com' },
|
||||
{ target: 'www.icloud.com:443', sni: 'www.icloud.com' },
|
||||
{ target: 'www.amazon.com:443', sni: 'www.amazon.com' },
|
||||
{ target: 'aws.amazon.com:443', sni: 'aws.amazon.com' },
|
||||
{ target: 'www.oracle.com:443', sni: 'www.oracle.com' },
|
||||
{ target: 'www.nvidia.com:443', sni: 'www.nvidia.com' },
|
||||
{ target: 'www.amd.com:443', sni: 'www.amd.com' },
|
||||
{ target: 'www.intel.com:443', sni: 'www.intel.com' },
|
||||
{ target: 'www.tesla.com:443', sni: 'www.tesla.com' },
|
||||
{ target: 'www.sony.com:443', sni: 'www.sony.com' }
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -28,4 +25,3 @@ function getRandomRealityTarget() {
|
|||
sni: selected.sni
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@ class AllSetting {
|
|||
this.subEnable = true;
|
||||
this.subJsonEnable = false;
|
||||
this.subTitle = "";
|
||||
this.subSupportUrl = "";
|
||||
this.subProfileUrl = "";
|
||||
this.subAnnounce = "";
|
||||
this.subEnableRouting = true;
|
||||
this.subRoutingRules = "";
|
||||
this.subListen = "";
|
||||
this.subPort = 2096;
|
||||
this.subPath = "/sub/";
|
||||
|
|
|
|||
|
|
@ -138,14 +138,14 @@
|
|||
return `streisand://import/${encodeURIComponent(this.app.subUrl)}`;
|
||||
},
|
||||
v2raytunUrl() {
|
||||
return this.app.subUrl;
|
||||
return this.app.subUrl;
|
||||
},
|
||||
npvtunUrl() {
|
||||
return this.app.subUrl;
|
||||
return this.app.subUrl;
|
||||
},
|
||||
happUrl() {
|
||||
return `happ://add/${encodeURIComponent(this.app.subUrl)}`;
|
||||
}
|
||||
happUrl() {
|
||||
return `happ://add/${encodeURIComponent(this.app.subUrl)}`;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
renderLink,
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ class RandomUtil {
|
|||
let length = 32;
|
||||
|
||||
if ([SSMethods.BLAKE3_AES_128_GCM].includes(method)) {
|
||||
length = 16;
|
||||
length = 16;
|
||||
}
|
||||
|
||||
const array = new Uint8Array(length);
|
||||
|
|
@ -154,28 +154,28 @@ class RandomUtil {
|
|||
|
||||
static randomBase32String(length = 16) {
|
||||
const array = new Uint8Array(length);
|
||||
|
||||
|
||||
window.crypto.getRandomValues(array);
|
||||
|
||||
|
||||
const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||
let result = '';
|
||||
let bits = 0;
|
||||
let buffer = 0;
|
||||
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
buffer = (buffer << 8) | array[i];
|
||||
bits += 8;
|
||||
|
||||
|
||||
while (bits >= 5) {
|
||||
bits -= 5;
|
||||
result += base32Chars[(buffer >>> bits) & 0x1F];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (bits > 0) {
|
||||
result += base32Chars[(buffer << (5 - bits)) & 0x1F];
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -908,7 +908,10 @@ class IntlUtil {
|
|||
const language = LanguageManager.getLanguage()
|
||||
const now = new Date()
|
||||
|
||||
const diff = Math.round((date - now) / (1000 * 60 * 60 * 24))
|
||||
// Handle delayed start (negative expiryTime values)
|
||||
const diff = date < 0
|
||||
? Math.round(date / (1000 * 60 * 60 * 24))
|
||||
: Math.round((date - now) / (1000 * 60 * 60 * 24))
|
||||
const formatter = new Intl.RelativeTimeFormat(language, { numeric: 'auto' })
|
||||
|
||||
return formatter.format(diff, 'day');
|
||||
|
|
|
|||
|
|
@ -14,10 +14,12 @@ class WebSocketClient {
|
|||
}
|
||||
|
||||
connect() {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.shouldReconnect = true;
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
// Ensure basePath ends with '/' for proper URL construction
|
||||
let basePath = this.basePath || '';
|
||||
|
|
@ -97,7 +99,10 @@ class WebSocketClient {
|
|||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, []);
|
||||
}
|
||||
this.listeners.get(event).push(callback);
|
||||
const callbacks = this.listeners.get(event);
|
||||
if (!callbacks.includes(callback)) {
|
||||
callbacks.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
off(event, callback) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
|
|
@ -193,6 +194,37 @@ func (a *InboundController) getClientIps(c *gin.Context) {
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -210,10 +210,10 @@ func (a *ServerController) getXrayLogs(c *gin.Context) {
|
|||
//getting tags for freedom and blackhole outbounds
|
||||
config, err := a.settingService.GetDefaultXrayConfig()
|
||||
if err == nil && config != nil {
|
||||
if cfgMap, ok := config.(map[string]interface{}); ok {
|
||||
if outbounds, ok := cfgMap["outbounds"].([]interface{}); ok {
|
||||
if cfgMap, ok := config.(map[string]any); ok {
|
||||
if outbounds, ok := cfgMap["outbounds"].([]any); ok {
|
||||
for _, outbound := range outbounds {
|
||||
if obMap, ok := outbound.(map[string]interface{}); ok {
|
||||
if obMap, ok := outbound.(map[string]any); ok {
|
||||
switch obMap["protocol"] {
|
||||
case "freedom":
|
||||
if tag, ok := obMap["tag"].(string); ok {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
|
@ -34,9 +37,10 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
|||
g.POST("/warp/:action", a.warp)
|
||||
g.POST("/update", a.updateSetting)
|
||||
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
|
||||
g.POST("/testOutbound", a.testOutbound)
|
||||
}
|
||||
|
||||
// getXraySetting retrieves the Xray configuration template and inbound tags.
|
||||
// getXraySetting retrieves the Xray configuration template, inbound tags, and outbound test URL.
|
||||
func (a *XraySettingController) getXraySetting(c *gin.Context) {
|
||||
xraySetting, err := a.SettingService.GetXrayConfigTemplate()
|
||||
if err != nil {
|
||||
|
|
@ -48,15 +52,36 @@ func (a *XraySettingController) getXraySetting(c *gin.Context) {
|
|||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
xrayResponse := "{ \"xraySetting\": " + xraySetting + ", \"inboundTags\": " + inboundTags + " }"
|
||||
jsonObj(c, xrayResponse, nil)
|
||||
outboundTestUrl, _ := a.SettingService.GetXrayOutboundTestUrl()
|
||||
if outboundTestUrl == "" {
|
||||
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.
|
||||
func (a *XraySettingController) updateSetting(c *gin.Context) {
|
||||
xraySetting := c.PostForm("xraySetting")
|
||||
err := a.XraySettingService.SaveXraySetting(xraySetting)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
if err := a.XraySettingService.SaveXraySetting(xraySetting); err != nil {
|
||||
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.
|
||||
|
|
@ -118,3 +143,26 @@ func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) {
|
|||
}
|
||||
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,6 +57,11 @@ type AllSetting struct {
|
|||
SubEnable bool `json:"subEnable" form:"subEnable"` // Enable subscription server
|
||||
SubJsonEnable bool `json:"subJsonEnable" form:"subJsonEnable"` // Enable JSON subscription endpoint
|
||||
SubTitle string `json:"subTitle" form:"subTitle"` // Subscription title
|
||||
SubSupportUrl string `json:"subSupportUrl" form:"subSupportUrl"` // Subscription support URL
|
||||
SubProfileUrl string `json:"subProfileUrl" form:"subProfileUrl"` // Subscription profile URL
|
||||
SubAnnounce string `json:"subAnnounce" form:"subAnnounce"` // Subscription announce
|
||||
SubEnableRouting bool `json:"subEnableRouting" form:"subEnableRouting"` // Enable routing for subscription
|
||||
SubRoutingRules string `json:"subRoutingRules" form:"subRoutingRules"` // Subscription global routing rules (Only for Happ)
|
||||
SubListen string `json:"subListen" form:"subListen"` // Subscription server listen IP
|
||||
SubPort int `json:"subPort" form:"subPort"` // Subscription server port
|
||||
SubPath string `json:"subPath" form:"subPath"` // Base path for subscription URLs
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ var (
|
|||
type WebServer interface {
|
||||
GetCron() *cron.Cron // Get the cron scheduler
|
||||
GetCtx() context.Context // Get the server context
|
||||
GetWSHub() interface{} // Get the WebSocket hub (using interface{} to avoid circular dependency)
|
||||
GetWSHub() any // Get the WebSocket hub (using any to avoid circular dependency)
|
||||
}
|
||||
|
||||
// SubServer interface defines methods for accessing the subscription server instance.
|
||||
|
|
|
|||
|
|
@ -24,6 +24,40 @@
|
|||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
/* mobile touch scrolling for tabs */
|
||||
@media (max-width: 576px) {
|
||||
.ant-tabs-nav-container {
|
||||
overflow-x: auto !important;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-behavior: smooth;
|
||||
overscroll-behavior-x: contain;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
padding: 0 !important; /* Remove padding for arrows */
|
||||
}
|
||||
.ant-tabs-nav-wrap {
|
||||
overflow: visible !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.ant-tabs-nav-scroll {
|
||||
overflow: visible !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.ant-tabs-nav {
|
||||
display: flex !important;
|
||||
transform: none !important; /* Disable JS transform */
|
||||
width: auto !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
.ant-tabs-tab-prev,
|
||||
.ant-tabs-tab-next {
|
||||
display: none !important; /* Hide arrows */
|
||||
}
|
||||
.ant-tabs-nav-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<title>{{ .host }} – {{ i18n .title}}</title>
|
||||
{{ end }}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{{define "form/inbound"}}
|
||||
<!-- base -->
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "enable" }}'>
|
||||
<a-switch v-model="dbInbound.enable"></a-switch>
|
||||
</a-form-item>
|
||||
|
|
@ -9,8 +10,10 @@
|
|||
</a-form-item>
|
||||
|
||||
<a-form-item label='{{ i18n "protocol" }}'>
|
||||
<a-select v-model="inbound.protocol" :disabled="isEdit" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
|
||||
<a-select v-model="inbound.protocol" :disabled="isEdit"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
|
|
@ -28,7 +31,8 @@
|
|||
</a-form-item>
|
||||
|
||||
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
|
||||
<a-input-number v-model.number="inbound.port" :min="1" :max="65535"></a-input-number>
|
||||
<a-input-number v-model.number="inbound.port" :min="1"
|
||||
:max="65535"></a-input-number>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
|
|
@ -41,29 +45,42 @@
|
|||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number v-model.number="dbInbound.totalGB" :min="0"></a-input-number>
|
||||
<a-input-number v-model.number="dbInbound.totalGB"
|
||||
:min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.periodicTrafficResetDesc" }}</span>
|
||||
<br v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
|
||||
<span v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
|
||||
<span>{{ i18n "pages.inbounds.periodicTrafficResetDesc"
|
||||
}}</span>
|
||||
<br
|
||||
v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
|
||||
<span
|
||||
v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
|
||||
<strong>{{ i18n "pages.inbounds.lastReset" }}:</strong>
|
||||
<span>[[ IntlUtil.formatDate(dbInbound.lastTrafficResetTime) ]]</span>
|
||||
<span>[[
|
||||
IntlUtil.formatDate(dbInbound.lastTrafficResetTime)
|
||||
]]</span>
|
||||
</span>
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.periodicTrafficResetTitle" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-select v-model="dbInbound.trafficReset" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="never">{{ i18n "pages.inbounds.periodicTrafficReset.never" }}</a-select-option>
|
||||
<a-select-option value="daily">{{ i18n "pages.inbounds.periodicTrafficReset.daily" }}</a-select-option>
|
||||
<a-select-option value="weekly">{{ i18n "pages.inbounds.periodicTrafficReset.weekly" }}</a-select-option>
|
||||
<a-select-option value="monthly">{{ i18n "pages.inbounds.periodicTrafficReset.monthly" }}</a-select-option>
|
||||
<a-select v-model="dbInbound.trafficReset"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="never">{{ i18n
|
||||
"pages.inbounds.periodicTrafficReset.never" }}</a-select-option>
|
||||
<a-select-option value="daily">{{ i18n
|
||||
"pages.inbounds.periodicTrafficReset.daily" }}</a-select-option>
|
||||
<a-select-option value="weekly">{{ i18n
|
||||
"pages.inbounds.periodicTrafficReset.weekly"
|
||||
}}</a-select-option>
|
||||
<a-select-option value="monthly">{{ i18n
|
||||
"pages.inbounds.periodicTrafficReset.monthly"
|
||||
}}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
|
|
@ -71,16 +88,20 @@
|
|||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire"
|
||||
}}</span>
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.expireDate" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-date-picker :style="{ width: '100%' }" v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }"
|
||||
format="YYYY-MM-DD HH:mm:ss" :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
<a-date-picker :style="{ width: '100%' }"
|
||||
v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="dbInbound._expiryTime"></a-date-picker>
|
||||
<a-persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
|
||||
<a-persian-datepicker v-else
|
||||
placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
|
||||
value="dbInbound._expiryTime" v-model="dbInbound._expiryTime">
|
||||
</a-persian-datepicker>
|
||||
</a-form-item>
|
||||
|
|
@ -126,6 +147,11 @@
|
|||
{{template "form/wireguard"}}
|
||||
</template>
|
||||
|
||||
<!-- tun -->
|
||||
<template v-if="inbound.protocol === Protocols.TUN">
|
||||
{{template "form/tun"}}
|
||||
</template>
|
||||
|
||||
<!-- stream settings -->
|
||||
<template v-if="inbound.canEnableStream()">
|
||||
{{template "form/streamSettings"}}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
{{define "form/outbound"}}
|
||||
<!-- base -->
|
||||
<a-tabs :active-key="outModal.activeKey" :style="{ padding: '0', backgroundColor: 'transparent' }"
|
||||
<a-tabs :active-key="outModal.activeKey"
|
||||
:style="{ padding: '0', backgroundColor: 'transparent' }"
|
||||
@change="(activeKey) => {outModal.toggleJson(activeKey == '2'); }">
|
||||
<a-tab-pane key="1" tab="Form">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "protocol" }}'>
|
||||
<a-select v-model="outbound.protocol" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="x,y in Protocols" :value="x">[[ y ]]</a-select-option>
|
||||
<a-select v-model="outbound.protocol"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="x,y in Protocols" :value="x">[[ y
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.tag" }}' has-feedback
|
||||
|
|
@ -21,8 +25,10 @@
|
|||
<!-- freedom settings-->
|
||||
<template v-if="outbound.protocol === Protocols.Freedom">
|
||||
<a-form-item label='Strategy'>
|
||||
<a-select v-model="outbound.settings.domainStrategy" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in OutboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
|
||||
<a-select v-model="outbound.settings.domainStrategy"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in OutboundDomainStrategies" :value="s">[[
|
||||
s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Redirect'>
|
||||
|
|
@ -35,18 +41,22 @@
|
|||
</a-form-item>
|
||||
<template v-if="Object.keys(outbound.settings.fragment).length >0">
|
||||
<a-form-item label='Packets'>
|
||||
<a-select v-model="outbound.settings.fragment.packets" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['1-3','tlshello']" :value="s">[[ s ]]</a-select-option>
|
||||
<a-select v-model="outbound.settings.fragment.packets"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['1-3','tlshello']" :value="s">[[ s
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Length'>
|
||||
<a-input v-model.trim="outbound.settings.fragment.length"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Interval'>
|
||||
<a-input v-model.trim="outbound.settings.fragment.interval"></a-input>
|
||||
<a-input
|
||||
v-model.trim="outbound.settings.fragment.interval"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Max Split'>
|
||||
<a-input v-model.trim="outbound.settings.fragment.maxSplit"></a-input>
|
||||
<a-input
|
||||
v-model.trim="outbound.settings.fragment.maxSplit"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
|
|
@ -60,11 +70,13 @@
|
|||
<!-- Add Noise Button -->
|
||||
<template v-if="outbound.settings.noises.length > 0">
|
||||
<a-form-item label="Noises">
|
||||
<a-button icon="plus" type="primary" size="small" @click="outbound.settings.addNoise()"></a-button>
|
||||
<a-button icon="plus" type="primary" size="small"
|
||||
@click="outbound.settings.addNoise()"></a-button>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Noise Configurations -->
|
||||
<a-form v-for="(noise, index) in outbound.settings.noises" :key="index" :colon="false"
|
||||
<a-form v-for="(noise, index) in outbound.settings.noises"
|
||||
:key="index" :colon="false"
|
||||
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider :style="{ margin: '0' }"> Noise [[ index + 1 ]]
|
||||
<a-icon v-if="outbound.settings.noises.length > 1" type="delete"
|
||||
|
|
@ -72,8 +84,10 @@
|
|||
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item label='Type'>
|
||||
<a-select v-model="noise.type" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['rand','base64','str', 'hex']" :value="s">[[ s ]]</a-select-option>
|
||||
<a-select v-model="noise.type"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['rand','base64','str', 'hex']"
|
||||
:value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Packet'>
|
||||
|
|
@ -83,8 +97,10 @@
|
|||
<a-input v-model.trim="noise.delay"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Apply To'>
|
||||
<a-select v-model="noise.applyTo" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['ip','ipv4','ipv6']" :value="s">[[ s ]]</a-select-option>
|
||||
<a-select v-model="noise.applyTo"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['ip','ipv4','ipv6']" :value="s">[[
|
||||
s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
|
@ -94,8 +110,10 @@
|
|||
<!-- blackhole settings -->
|
||||
<template v-if="outbound.protocol === Protocols.Blackhole">
|
||||
<a-form-item label='Response Type'>
|
||||
<a-select v-model="outbound.settings.type" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['', 'none','http']" :value="s">[[ s ]]</a-select-option>
|
||||
<a-select v-model="outbound.settings.type"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['', 'none','http']" :value="s">[[ s
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
|
@ -103,16 +121,21 @@
|
|||
<!-- dns settings -->
|
||||
<template v-if="outbound.protocol === Protocols.DNS">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
|
||||
<a-select v-model="outbound.settings.network" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['udp','tcp']" :value="s">[[ s ]]</a-select-option>
|
||||
<a-select v-model="outbound.settings.network"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['udp','tcp']" :value="s">[[ s
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='non-IP queries'>
|
||||
<a-select v-model="outbound.settings.nonIPQuery" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['reject','drop','skip']" :value="s">[[ s ]]</a-select-option>
|
||||
<a-select v-model="outbound.settings.nonIPQuery"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['reject','drop','skip']" :value="s">[[
|
||||
s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="outbound.settings.nonIPQuery === 'skip'" label='Block Types'>
|
||||
<a-form-item v-if="outbound.settings.nonIPQuery === 'skip'"
|
||||
label='Block Types'>
|
||||
<a-input v-model.number="outbound.settings.blockTypes"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
|
@ -149,15 +172,19 @@
|
|||
<a-input disabled v-model="outbound.settings.pubKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.domainStrategy" }}'>
|
||||
<a-select v-model="outbound.settings.domainStrategy" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="wds in ['', ...WireguardDomainStrategy]" :value="wds">[[ wds ]]</a-select-option>
|
||||
<a-select v-model="outbound.settings.domainStrategy"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="wds in ['', ...WireguardDomainStrategy]"
|
||||
:value="wds">[[ wds ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='MTU'>
|
||||
<a-input-number v-model.number="outbound.settings.mtu" min="0"></a-input-number>
|
||||
<a-input-number v-model.number="outbound.settings.mtu"
|
||||
min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Workers'>
|
||||
<a-input-number v-model.number="outbound.settings.workers" min="0"></a-input-number>
|
||||
<a-input-number v-model.number="outbound.settings.workers"
|
||||
min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='No Kernel Tun'>
|
||||
<a-switch v-model="outbound.settings.noKernelTun"></a-switch>
|
||||
|
|
@ -173,11 +200,14 @@
|
|||
<a-input v-model="outbound.settings.reserved"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Peers">
|
||||
<a-button icon="plus" type="primary" size="small" @click="outbound.settings.addPeer()"></a-button>
|
||||
<a-button icon="plus" type="primary" size="small"
|
||||
@click="outbound.settings.addPeer()"></a-button>
|
||||
</a-form-item>
|
||||
<a-form v-for="(peer, index) in outbound.settings.peers" :colon="false" :label-col="{ md: {span:8} }"
|
||||
<a-form v-for="(peer, index) in outbound.settings.peers" :colon="false"
|
||||
:label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-divider :style="{ margin: '0' }"> Peer [[ index + 1 ]] <a-icon v-if="outbound.settings.peers.length>1"
|
||||
<a-divider :style="{ margin: '0' }"> Peer [[ index + 1 ]] <a-icon
|
||||
v-if="outbound.settings.peers.length>1"
|
||||
type="delete" @click="() => outbound.settings.delPeer(index)"
|
||||
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
||||
</a-divider>
|
||||
|
|
@ -193,17 +223,21 @@
|
|||
<a-form-item>
|
||||
<template slot="label">
|
||||
{{ i18n "pages.xray.wireguard.allowedIPs" }}
|
||||
<a-button icon="plus" type="primary" size="small" @click="peer.allowedIPs.push('')"></a-button>
|
||||
<a-button icon="plus" type="primary" size="small"
|
||||
@click="peer.allowedIPs.push('')"></a-button>
|
||||
</template>
|
||||
<template v-for="(aip, index) in peer.allowedIPs" :style="{ marginBottom: '10px' }">
|
||||
<template v-for="(aip, index) in peer.allowedIPs"
|
||||
:style="{ marginBottom: '10px' }">
|
||||
<a-input v-model.trim="peer.allowedIPs[index]">
|
||||
<a-button icon="minus" v-if="peer.allowedIPs.length>1" slot="addonAfter" size="small"
|
||||
<a-button icon="minus" v-if="peer.allowedIPs.length>1"
|
||||
slot="addonAfter" size="small"
|
||||
@click="peer.allowedIPs.splice(index, 1)"></a-button>
|
||||
</a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item label='Keep Alive'>
|
||||
<a-input-number v-model.number="peer.keepAlive" :min="0"></a-input-number>
|
||||
<a-input-number v-model.number="peer.keepAlive"
|
||||
:min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
|
@ -214,12 +248,14 @@
|
|||
<a-input v-model.trim="outbound.settings.address"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
|
||||
<a-input-number v-model.number="outbound.settings.port" :min="1" :max="65532"></a-input-number>
|
||||
<a-input-number v-model.number="outbound.settings.port" :min="1"
|
||||
:max="65532"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- VLESS/VMess user settings -->
|
||||
<template v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)">
|
||||
<template
|
||||
v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)">
|
||||
<a-form-item label='ID'>
|
||||
<a-input v-model.trim="outbound.settings.id"></a-input>
|
||||
</a-form-item>
|
||||
|
|
@ -227,8 +263,10 @@
|
|||
<!-- vmess settings -->
|
||||
<template v-if="outbound.protocol === Protocols.VMess">
|
||||
<a-form-item label='Security'>
|
||||
<a-select v-model="outbound.settings.security" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
|
||||
<a-select v-model="outbound.settings.security"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
|
@ -241,35 +279,47 @@
|
|||
</template>
|
||||
<template v-if="outbound.canEnableTlsFlow()">
|
||||
<a-form-item label='Flow'>
|
||||
<a-select v-model="outbound.settings.flow" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
<a-select v-model="outbound.settings.flow"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value selected>{{ i18n "none"
|
||||
}}</a-select-option>
|
||||
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[
|
||||
key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<!-- XTLS Vision Advanced Settings -->
|
||||
<template v-if="outbound.canEnableVisionSeed()">
|
||||
<a-form-item label="Vision Pre-Connect">
|
||||
<a-input-number v-model.number="outbound.settings.testpre" :min="0" :max="10" :style="{ width: '100%' }"
|
||||
<a-input-number v-model.number="outbound.settings.testpre" :min="0"
|
||||
:max="10" :style="{ width: '100%' }"
|
||||
placeholder="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="Vision Seed">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="6">
|
||||
<a-input-number v-model.number="outbound.settings.testseed[0]" :min="0" :max="9999"
|
||||
:style="{ width: '100%' }" placeholder="900" addon-before="[0]"></a-input-number>
|
||||
<a-input-number v-model.number="outbound.settings.testseed[0]"
|
||||
:min="0" :max="9999"
|
||||
:style="{ width: '100%' }" placeholder="900"
|
||||
addon-before="[0]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number v-model.number="outbound.settings.testseed[1]" :min="0" :max="9999"
|
||||
:style="{ width: '100%' }" placeholder="500" addon-before="[1]"></a-input-number>
|
||||
<a-input-number v-model.number="outbound.settings.testseed[1]"
|
||||
:min="0" :max="9999"
|
||||
:style="{ width: '100%' }" placeholder="500"
|
||||
addon-before="[1]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number v-model.number="outbound.settings.testseed[2]" :min="0" :max="9999"
|
||||
:style="{ width: '100%' }" placeholder="900" addon-before="[2]"></a-input-number>
|
||||
<a-input-number v-model.number="outbound.settings.testseed[2]"
|
||||
:min="0" :max="9999"
|
||||
:style="{ width: '100%' }" placeholder="900"
|
||||
addon-before="[2]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number v-model.number="outbound.settings.testseed[3]" :min="0" :max="9999"
|
||||
:style="{ width: '100%' }" placeholder="256" addon-before="[3]"></a-input-number>
|
||||
<a-input-number v-model.number="outbound.settings.testseed[3]"
|
||||
:min="0" :max="9999"
|
||||
:style="{ width: '100%' }" placeholder="256"
|
||||
addon-before="[3]"></a-input-number>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
|
|
@ -289,7 +339,8 @@
|
|||
</template>
|
||||
|
||||
<!-- trojan/shadowsocks -->
|
||||
<template v-if="[Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
|
||||
<template
|
||||
v-if="[Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="outbound.settings.password"></a-input>
|
||||
</a-form-item>
|
||||
|
|
@ -298,8 +349,10 @@
|
|||
<!-- shadowsocks -->
|
||||
<template v-if="outbound.protocol === Protocols.Shadowsocks">
|
||||
<a-form-item label='{{ i18n "encryption" }}'>
|
||||
<a-select v-model="outbound.settings.method" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="(method, method_name) in SSMethods" :value="method">[[ method_name
|
||||
<a-select v-model="outbound.settings.method"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="(method, method_name) in SSMethods"
|
||||
:value="method">[[ method_name
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
|
@ -307,15 +360,25 @@
|
|||
<a-switch v-model="outbound.settings.uot"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='UoTVersion'>
|
||||
<a-input-number v-model.number="outbound.settings.UoTVersion" :min="1" :max="2"></a-input-number>
|
||||
<a-input-number v-model.number="outbound.settings.UoTVersion"
|
||||
:min="1" :max="2"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- hysteria settings -->
|
||||
<template v-if="outbound.protocol === Protocols.Hysteria">
|
||||
<a-form-item label='Version'>
|
||||
<a-input-number v-model.number="outbound.settings.version" :min="2"
|
||||
:max="2" disabled></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- stream settings -->
|
||||
<template v-if="outbound.canEnableStream()">
|
||||
<a-form-item label='{{ i18n "transmission" }}'>
|
||||
<a-select v-model="outbound.stream.network" @change="streamNetworkChange"
|
||||
<a-select v-model="outbound.stream.network"
|
||||
@change="streamNetworkChange"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="tcp">TCP (RAW)</a-select-option>
|
||||
<a-select-option value="kcp">mKCP</a-select-option>
|
||||
|
|
@ -323,6 +386,8 @@
|
|||
<a-select-option value="grpc">gRPC</a-select-option>
|
||||
<a-select-option value="httpupgrade">HTTPUpgrade</a-select-option>
|
||||
<a-select-option value="xhttp">XHTTP</a-select-option>
|
||||
<a-select-option v-if="outbound.protocol === Protocols.Hysteria"
|
||||
value="hysteria">Hysteria2</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.network === 'tcp'">
|
||||
|
|
@ -342,40 +407,32 @@
|
|||
|
||||
<!-- kcp -->
|
||||
<template v-if="outbound.stream.network === 'kcp'">
|
||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||
<a-select v-model="outbound.stream.kcp.type" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="none">None</a-select-option>
|
||||
<a-select-option value="srtp">SRTP</a-select-option>
|
||||
<a-select-option value="utp">uTP</a-select-option>
|
||||
<a-select-option value="wechat-video">WeChat</a-select-option>
|
||||
<a-select-option value="dtls">DTLS 1.2</a-select-option>
|
||||
<a-select-option value="wireguard">WireGuard</a-select-option>
|
||||
<a-select-option value="dns">DNS</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model="outbound.stream.kcp.seed"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='MTU'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.mtu" min="0"></a-input-number>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.mtu"
|
||||
min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='TTI (ms)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.tti" min="0"></a-input-number>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.tti"
|
||||
min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Uplink (MB/s)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.upCap" min="0"></a-input-number>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.upCap"
|
||||
min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Downlink (MB/s)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.downCap" min="0"></a-input-number>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.downCap"
|
||||
min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Congestion'>
|
||||
<a-switch v-model="outbound.stream.kcp.congestion"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='Read Buffer (MB)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.readBuffer" min="0"></a-input-number>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.readBuffer"
|
||||
min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Write Buffer (MB)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.writeBuffer" min="0"></a-input-number>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.writeBuffer"
|
||||
min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
|
|
@ -388,7 +445,8 @@
|
|||
<a-input v-model.trim="outbound.stream.ws.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Heartbeat Period'>
|
||||
<a-input-number v-model.number="outbound.stream.ws.heartbeatPeriod" :min="0"></a-input-number>
|
||||
<a-input-number v-model.number="outbound.stream.ws.heartbeatPeriod"
|
||||
:min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
|
|
@ -424,45 +482,199 @@
|
|||
<a-input v-model.trim="outbound.stream.xhttp.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Mode'>
|
||||
<a-select v-model="outbound.stream.xhttp.mode" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in MODE_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
<a-select v-model="outbound.stream.xhttp.mode"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in MODE_OPTION" :value="key">[[ key
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="No gRPC Header"
|
||||
v-if="outbound.stream.xhttp.mode === 'stream-up' || outbound.stream.xhttp.mode === 'stream-one'">
|
||||
<a-switch v-model="outbound.stream.xhttp.noGRPCHeader"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Min Upload Interval (Ms)" v-if="outbound.stream.xhttp.mode === 'packet-up'">
|
||||
<a-input v-model.trim="outbound.stream.xhttp.scMinPostsIntervalMs"></a-input>
|
||||
<a-form-item label="Min Upload Interval (Ms)"
|
||||
v-if="outbound.stream.xhttp.mode === 'packet-up'">
|
||||
<a-input
|
||||
v-model.trim="outbound.stream.xhttp.scMinPostsIntervalMs"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Concurrency" v-if="!outbound.stream.xhttp.xmux.maxConnections">
|
||||
<a-input v-model="outbound.stream.xhttp.xmux.maxConcurrency"></a-input>
|
||||
<a-form-item label="Max Concurrency"
|
||||
v-if="!outbound.stream.xhttp.xmux.maxConnections">
|
||||
<a-input
|
||||
v-model="outbound.stream.xhttp.xmux.maxConcurrency"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Connections" v-if="!outbound.stream.xhttp.xmux.maxConcurrency">
|
||||
<a-input v-model="outbound.stream.xhttp.xmux.maxConnections"></a-input>
|
||||
<a-form-item label="Max Connections"
|
||||
v-if="!outbound.stream.xhttp.xmux.maxConcurrency">
|
||||
<a-input
|
||||
v-model="outbound.stream.xhttp.xmux.maxConnections"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Reuse Times">
|
||||
<a-input v-model="outbound.stream.xhttp.xmux.cMaxReuseTimes"></a-input>
|
||||
<a-input
|
||||
v-model="outbound.stream.xhttp.xmux.cMaxReuseTimes"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Request Times">
|
||||
<a-input v-model="outbound.stream.xhttp.xmux.hMaxRequestTimes"></a-input>
|
||||
<a-input
|
||||
v-model="outbound.stream.xhttp.xmux.hMaxRequestTimes"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Reusable Secs">
|
||||
<a-input v-model="outbound.stream.xhttp.xmux.hMaxReusableSecs"></a-input>
|
||||
<a-input
|
||||
v-model="outbound.stream.xhttp.xmux.hMaxReusableSecs"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Keep Alive Period'>
|
||||
<a-input-number v-model.number="outbound.stream.xhttp.xmux.hKeepAlivePeriod"></a-input-number>
|
||||
<a-input-number
|
||||
v-model.number="outbound.stream.xhttp.xmux.hKeepAlivePeriod"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- hysteria -->
|
||||
<template v-if="outbound.stream.network === 'hysteria'">
|
||||
<a-form-item label='Auth Password'>
|
||||
<a-input v-model.trim="outbound.stream.hysteria.auth"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Congestion'>
|
||||
<a-select v-model="outbound.stream.hysteria.congestion"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>BBR (Auto)</a-select-option>
|
||||
<a-select-option value="brutal">Brutal</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Upload Speed'>
|
||||
<a-input v-model.trim="outbound.stream.hysteria.up"
|
||||
placeholder="0 (BBR mode), e.g., 100 mbps"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Download Speed'>
|
||||
<a-input v-model.trim="outbound.stream.hysteria.down"
|
||||
placeholder="0 (BBR mode), e.g., 100 mbps"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='UDP Hop Port'>
|
||||
<a-input v-model.trim="outbound.stream.hysteria.udphopPort"
|
||||
placeholder="e.g., 1145-1919 or 11,13,15-17"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='UDP Hop Interval Min (s)'
|
||||
v-if="outbound.stream.hysteria.udphopPort">
|
||||
<a-input-number
|
||||
v-model.number="outbound.stream.hysteria.udphopIntervalMin"
|
||||
:min="5"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='UDP Hop Interval Max (s)'
|
||||
v-if="outbound.stream.hysteria.udphopPort">
|
||||
<a-input-number
|
||||
v-model.number="outbound.stream.hysteria.udphopIntervalMax"
|
||||
:min="5"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Init Stream Receive'>
|
||||
<a-input-number
|
||||
v-model.number="outbound.stream.hysteria.initStreamReceiveWindow"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Max Stream Receive'>
|
||||
<a-input-number
|
||||
v-model.number="outbound.stream.hysteria.maxStreamReceiveWindow"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Init Connection Receive'>
|
||||
<a-input-number
|
||||
v-model.number="outbound.stream.hysteria.initConnectionReceiveWindow"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Max Connection Receive'>
|
||||
<a-input-number
|
||||
v-model.number="outbound.stream.hysteria.maxConnectionReceiveWindow"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Max Idle Timeout (s)'>
|
||||
<a-input-number
|
||||
v-model.number="outbound.stream.hysteria.maxIdleTimeout" :min="4"
|
||||
:max="120"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Keep Alive Period (s)'>
|
||||
<a-input-number
|
||||
v-model.number="outbound.stream.hysteria.keepAlivePeriod" :min="0"
|
||||
:max="60"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Disable Path MTU'>
|
||||
<a-switch
|
||||
v-model="outbound.stream.hysteria.disablePathMTUDiscovery"></a-switch>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- finalmask settings -->
|
||||
<template v-if="outbound.canEnableStream()">
|
||||
<a-form-item label="UDP Masks">
|
||||
<a-button icon="plus" type="primary" size="small"
|
||||
@click="outbound.stream.addUdpMask(outbound.protocol === Protocols.Hysteria ? 'salamander' : (outbound.stream.network === 'kcp' ? 'mkcp-aes128gcm' : 'xdns'))"></a-button>
|
||||
</a-form-item>
|
||||
<template
|
||||
v-if="outbound.stream.finalmask.udp && outbound.stream.finalmask.udp.length > 0">
|
||||
<a-form v-for="(mask, index) in outbound.stream.finalmask.udp"
|
||||
:key="index" :colon="false"
|
||||
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider :style="{ margin: '0' }"> UDP Mask [[ index + 1 ]]
|
||||
<a-icon type="delete"
|
||||
@click="() => outbound.stream.delUdpMask(index)"
|
||||
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item label='Type'>
|
||||
<a-select v-model="mask.type"
|
||||
@change="(type) => mask.settings = mask._getDefaultSettings(type, {})"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<!-- Salamander for Hysteria2 only -->
|
||||
<a-select-option v-if="outbound.protocol === Protocols.Hysteria"
|
||||
value="salamander">
|
||||
Salamander (Hysteria2)</a-select-option>
|
||||
<!-- mKCP-specific masks -->
|
||||
<a-select-option v-if="outbound.stream.network === 'kcp'"
|
||||
value="mkcp-aes128gcm">
|
||||
mKCP AES-128-GCM</a-select-option>
|
||||
<a-select-option v-if="outbound.stream.network === 'kcp'"
|
||||
value="header-dns">
|
||||
Header DNS</a-select-option>
|
||||
<a-select-option v-if="outbound.stream.network === 'kcp'"
|
||||
value="header-dtls">
|
||||
Header DTLS 1.2</a-select-option>
|
||||
<a-select-option v-if="outbound.stream.network === 'kcp'"
|
||||
value="header-srtp">
|
||||
Header SRTP</a-select-option>
|
||||
<a-select-option v-if="outbound.stream.network === 'kcp'"
|
||||
value="header-utp">
|
||||
Header uTP</a-select-option>
|
||||
<a-select-option v-if="outbound.stream.network === 'kcp'"
|
||||
value="header-wechat">
|
||||
Header WeChat Video</a-select-option>
|
||||
<a-select-option v-if="outbound.stream.network === 'kcp'"
|
||||
value="header-wireguard">
|
||||
Header WireGuard</a-select-option>
|
||||
<a-select-option v-if="outbound.stream.network === 'kcp'"
|
||||
value="mkcp-original">
|
||||
mKCP Original</a-select-option>
|
||||
<!-- xDNS for TCP/WS/HTTPUpgrade/XHTTP -->
|
||||
<a-select-option
|
||||
v-if="['tcp', 'ws', 'httpupgrade', 'xhttp'].includes(outbound.stream.network)"
|
||||
value="xdns">
|
||||
xDNS (Experimental)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<!-- Settings for password-based masks -->
|
||||
<a-form-item label='Password'
|
||||
v-if="['salamander', 'mkcp-aes128gcm'].includes(mask.type)">
|
||||
<a-input v-model.trim="mask.settings.password"
|
||||
placeholder="Obfuscation password"></a-input>
|
||||
</a-form-item>
|
||||
<!-- Settings for domain-based masks -->
|
||||
<a-form-item label='Domain'
|
||||
v-if="['header-dns', 'xdns'].includes(mask.type)">
|
||||
<a-input v-model.trim="mask.settings.domain"
|
||||
placeholder="e.g., www.example.com"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- tls settings -->
|
||||
<template v-if="outbound.canEnableTls()">
|
||||
<a-form-item label='{{ i18n "security" }}'>
|
||||
<a-radio-group v-model="outbound.stream.security" button-style="solid">
|
||||
<a-radio-group v-model="outbound.stream.security"
|
||||
button-style="solid">
|
||||
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
|
||||
<a-radio-button value="tls">TLS</a-radio-button>
|
||||
<a-radio-button v-if="outbound.canEnableReality()" value="reality">Reality</a-radio-button>
|
||||
<a-radio-button v-if="outbound.canEnableReality()"
|
||||
value="reality">Reality</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.isTls">
|
||||
|
|
@ -470,33 +682,47 @@
|
|||
<a-input v-model.trim="outbound.stream.tls.serverName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model="outbound.stream.tls.fingerprint" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value=''>None</a-select-option>
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
<a-select v-model="outbound.stream.tls.fingerprint"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>None</a-select-option>
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[
|
||||
key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="ALPN">
|
||||
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
<a-select mode="multiple"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="outbound.stream.tls.alpn">
|
||||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
|
||||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="ECH Config List">
|
||||
<a-input v-model.trim="outbound.stream.tls.echConfigList"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Allow Insecure">
|
||||
<a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch>
|
||||
<a-form-item label="verify Peer Cert By Name">
|
||||
<a-input
|
||||
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>
|
||||
</template>
|
||||
|
||||
<!-- reality settings -->
|
||||
<template v-if="outbound.stream.isReality">
|
||||
<a-form-item label="SNI">
|
||||
<a-input v-model.trim="outbound.stream.reality.serverName"></a-input>
|
||||
<a-input
|
||||
v-model.trim="outbound.stream.reality.serverName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model="outbound.stream.reality.fingerprint" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
<a-select v-model="outbound.stream.reality.fingerprint"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[
|
||||
key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Short ID">
|
||||
|
|
@ -506,10 +732,12 @@
|
|||
<a-input v-model.trim="outbound.stream.reality.spiderX"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Public Key">
|
||||
<a-textarea v-model.trim="outbound.stream.reality.publicKey"></a-textarea>
|
||||
<a-textarea
|
||||
v-model.trim="outbound.stream.reality.publicKey"></a-textarea>
|
||||
</a-form-item>
|
||||
<a-form-item label="mldsa65 Verify">
|
||||
<a-textarea v-model.trim="outbound.stream.reality.mldsa65Verify"></a-textarea>
|
||||
<a-textarea
|
||||
v-model.trim="outbound.stream.reality.mldsa65Verify"></a-textarea>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
|
@ -520,34 +748,44 @@
|
|||
</a-form-item>
|
||||
<template v-if="outbound.stream.sockoptSwitch">
|
||||
<a-form-item label="Dialer Proxy">
|
||||
<a-select v-model="outbound.stream.sockopt.dialerProxy" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="tag in ['', ...outModal.tags]" :value="tag">[[ tag ]]</a-select-option>
|
||||
<a-select v-model="outbound.stream.sockopt.dialerProxy"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="tag in ['', ...outModal.tags]"
|
||||
:value="tag">[[ tag ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Address Port Strategy'>
|
||||
<a-select v-model="outbound.stream.sockopt.addressPortStrategy"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in Address_Port_Strategy" :value="key">[[ key ]]</a-select-option>
|
||||
<a-select-option v-for="key in Address_Port_Strategy"
|
||||
:value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Keep Alive Interval">
|
||||
<a-input-number v-model.number="outbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number>
|
||||
<a-input-number
|
||||
v-model.number="outbound.stream.sockopt.tcpKeepAliveInterval"
|
||||
:min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="TCP Fast Open">
|
||||
<a-switch v-model="outbound.stream.sockopt.tcpFastOpen"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Multipath TCP">
|
||||
<a-switch v-model.trim="outbound.stream.sockopt.tcpMptcp"></a-switch>
|
||||
<a-switch
|
||||
v-model.trim="outbound.stream.sockopt.tcpMptcp"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Penetrate">
|
||||
<a-switch v-model="outbound.stream.sockopt.penetrate"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Trusted X-Forwarded-For">
|
||||
<a-select mode="tags" v-model="outbound.stream.sockopt.trustedXForwardedFor" :style="{ width: '100%' }"
|
||||
<a-select mode="tags"
|
||||
v-model="outbound.stream.sockopt.trustedXForwardedFor"
|
||||
:style="{ width: '100%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="CF-Connecting-IP">CF-Connecting-IP</a-select-option>
|
||||
<a-select-option
|
||||
value="CF-Connecting-IP">CF-Connecting-IP</a-select-option>
|
||||
<a-select-option value="X-Real-IP">X-Real-IP</a-select-option>
|
||||
<a-select-option value="True-Client-IP">True-Client-IP</a-select-option>
|
||||
<a-select-option
|
||||
value="True-Client-IP">True-Client-IP</a-select-option>
|
||||
<a-select-option value="X-Client-IP">X-Client-IP</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
|
@ -560,14 +798,19 @@
|
|||
</a-form-item>
|
||||
<template v-if="outbound.mux.enabled">
|
||||
<a-form-item label="Concurrency">
|
||||
<a-input-number v-model.number="outbound.mux.concurrency" :min="-1" :max="1024"></a-input-number>
|
||||
<a-input-number v-model.number="outbound.mux.concurrency"
|
||||
:min="-1"
|
||||
:max="1024"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="xudp Concurrency">
|
||||
<a-input-number v-model.number="outbound.mux.xudpConcurrency" :min="-1" :max="1024"></a-input-number>
|
||||
<a-input-number v-model.number="outbound.mux.xudpConcurrency"
|
||||
:min="-1" :max="1024"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="xudp UDP 443">
|
||||
<a-select v-model="outbound.mux.xudpProxyUDP443" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="c in ['reject', 'allow', 'skip']" :value="c">[[ c ]]</a-select-option>
|
||||
<a-select v-model="outbound.mux.xudpProxyUDP443"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="c in ['reject', 'allow', 'skip']"
|
||||
:value="c">[[ c ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
|
@ -576,11 +819,13 @@
|
|||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab="JSON" force-render="true">
|
||||
<a-space direction="vertical" :size="10" :style="{ marginTop: '10px' }">
|
||||
<a-input addon-before='{{ i18n "pages.xray.outbound.link" }}' v-model.trim="outModal.link"
|
||||
placeholder="vmess:// vless:// trojan:// ss://">
|
||||
<a-input addon-before='{{ i18n "pages.xray.outbound.link" }}'
|
||||
v-model.trim="outModal.link"
|
||||
placeholder="vmess:// vless:// trojan:// ss:// hysteria2://">
|
||||
<a-icon slot="addonAfter" type="form" @click="convertLink"></a-icon>
|
||||
</a-input>
|
||||
<textarea :style="{ position: 'absolute', left: '-800px' }" id="outboundJson"></textarea>
|
||||
<textarea :style="{ position: 'absolute', left: '-800px' }"
|
||||
id="outboundJson"></textarea>
|
||||
</a-space>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
|
|
|||
44
web/html/form/protocol/tun.html
Normal file
44
web/html/form/protocol/tun.html
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
{{define "form/tun"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.tun.nameDesc" }}</span>
|
||||
</template>
|
||||
Interface Name
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="inbound.settings.name"
|
||||
placeholder="xray0"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.tun.mtuDesc" }}</span>
|
||||
</template>
|
||||
MTU
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number v-model.number="inbound.settings.mtu" :min="1"
|
||||
:max="9000" placeholder="1500"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.tun.userLevelDesc" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.xray.tun.userLevel" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number v-model.number="inbound.settings.userLevel" :min="0"
|
||||
placeholder="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
{{define "form/vless"}}
|
||||
<a-collapse activeKey="0"
|
||||
v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||
{{template "form/client"}}
|
||||
</a-collapse-panel>
|
||||
|
|
@ -22,115 +21,103 @@
|
|||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<template v-if=" !inbound.stream.isTLS || !inbound.stream.isReality">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Authentication">
|
||||
<a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="X25519, not Post-Quantum">X25519 (not
|
||||
Post-Quantum)</a-select-option>
|
||||
<a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768
|
||||
(Post-Quantum)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="decryption">
|
||||
<a-input v-model.trim="inbound.settings.decryption"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="encryption">
|
||||
<a-input v-model="inbound.settings.encryption"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button type="primary" icon="import" @click="getNewVlessEnc">Get New
|
||||
keys</a-button>
|
||||
<a-button danger @click="clearVlessEnc">Clear</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider v-if="inbound.settings.selectedAuth"
|
||||
:style="{ margin: '5px 0' }"></a-divider>
|
||||
</template>
|
||||
<template v-if="inbound.isTcp && !inbound.settings.selectedAuth">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Fallbacks">
|
||||
<a-button icon="plus" type="primary" size="small"
|
||||
@click="inbound.settings.addFallback()"></a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Authentication">
|
||||
<a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="X25519, not Post-Quantum">X25519 (not
|
||||
Post-Quantum)</a-select-option>
|
||||
<a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768
|
||||
(Post-Quantum)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="decryption">
|
||||
<a-input v-model.trim="inbound.settings.decryption"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="encryption">
|
||||
<a-input v-model="inbound.settings.encryption"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button type="primary" icon="import" @click="getNewVlessEnc">Get New
|
||||
keys</a-button>
|
||||
<a-button danger @click="clearVlessEnc">Clear</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider :style="{ margin: '5px 0' }"></a-divider>
|
||||
</template>
|
||||
<template v-if="inbound.isTcp && !inbound.settings.selectedAuth">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Fallbacks">
|
||||
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vless fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false"
|
||||
:label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon
|
||||
type="delete"
|
||||
@click="() => inbound.settings.delFallback(index)"
|
||||
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item label='SNI'>
|
||||
<a-input v-model="fallback.name"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='ALPN'>
|
||||
<a-input v-model="fallback.alpn"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Path'>
|
||||
<a-input v-model="fallback.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Dest'>
|
||||
<a-input v-model="fallback.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='xVer'>
|
||||
<a-input-number v-model.number="fallback.xver" :min="0"
|
||||
:max="2"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider :style="{ margin: '5px 0' }"></a-divider>
|
||||
</template>
|
||||
<template v-if="inbound.canEnableVisionSeed()">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Vision Seed">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="6">
|
||||
<a-input-number
|
||||
:value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900"
|
||||
@change="(val) => updateTestseed(0, val)" :min="0" :max="9999"
|
||||
:style="{ width: '100%' }"
|
||||
placeholder="900" addon-before="[0]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number
|
||||
:value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500"
|
||||
@change="(val) => updateTestseed(1, val)" :min="0" :max="9999"
|
||||
:style="{ width: '100%' }"
|
||||
placeholder="500" addon-before="[1]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number
|
||||
:value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900"
|
||||
@change="(val) => updateTestseed(2, val)" :min="0" :max="9999"
|
||||
:style="{ width: '100%' }"
|
||||
placeholder="900" addon-before="[2]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number
|
||||
:value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256"
|
||||
@change="(val) => updateTestseed(3, val)" :min="0" :max="9999"
|
||||
:style="{ width: '100%' }"
|
||||
placeholder="256" addon-before="[3]"></a-input-number>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-space :size="8" :style="{ marginTop: '8px' }">
|
||||
<a-button type="primary" @click="setRandomTestseed">
|
||||
Rand
|
||||
</a-button>
|
||||
<a-button @click="resetTestseed">
|
||||
Reset
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider :style="{ margin: '5px 0' }"></a-divider>
|
||||
</template>
|
||||
<!-- vless fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete"
|
||||
@click="() => inbound.settings.delFallback(index)"
|
||||
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item label='SNI'>
|
||||
<a-input v-model="fallback.name"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='ALPN'>
|
||||
<a-input v-model="fallback.alpn"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Path'>
|
||||
<a-input v-model="fallback.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Dest'>
|
||||
<a-input v-model="fallback.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='xVer'>
|
||||
<a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider :style="{ margin: '5px 0' }"></a-divider>
|
||||
</template>
|
||||
<template v-if="inbound.canEnableVisionSeed()">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Vision Seed">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="6">
|
||||
<a-input-number
|
||||
:value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900"
|
||||
@change="(val) => updateTestseed(0, val)" :min="0" :max="9999" :style="{ width: '100%' }"
|
||||
placeholder="900" addon-before="[0]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number
|
||||
:value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500"
|
||||
@change="(val) => updateTestseed(1, val)" :min="0" :max="9999" :style="{ width: '100%' }"
|
||||
placeholder="500" addon-before="[1]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number
|
||||
:value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900"
|
||||
@change="(val) => updateTestseed(2, val)" :min="0" :max="9999" :style="{ width: '100%' }"
|
||||
placeholder="900" addon-before="[2]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number
|
||||
:value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256"
|
||||
@change="(val) => updateTestseed(3, val)" :min="0" :max="9999" :style="{ width: '100%' }"
|
||||
placeholder="256" addon-before="[3]"></a-input-number>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-space :size="8" :style="{ marginTop: '8px' }">
|
||||
<a-button type="primary" @click="setRandomTestseed">
|
||||
Rand
|
||||
</a-button>
|
||||
<a-button @click="resetTestseed">
|
||||
Reset
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider :style="{ margin: '5px 0' }"></a-divider>
|
||||
</template>
|
||||
{{end}}
|
||||
84
web/html/form/stream/stream_finalmask.html
Normal file
84
web/html/form/stream/stream_finalmask.html
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
{{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,48 +1,32 @@
|
|||
{{define "form/streamKCP"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||
<a-select v-model="inbound.stream.kcp.type" :style="{ width: '50%' }" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="none">None</a-select-option>
|
||||
<a-select-option value="srtp">SRTP</a-select-option>
|
||||
<a-select-option value="utp">uTP</a-select-option>
|
||||
<a-select-option value="wechat-video">WeChat</a-select-option>
|
||||
<a-select-option value="dtls">DTLS 1.2</a-select-option>
|
||||
<a-select-option value="wireguard">WireGuard</a-select-option>
|
||||
<a-select-option value="dns">DNS</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template>
|
||||
{{ i18n "password" }}
|
||||
<a-icon @click="inbound.stream.kcp.seed = RandomUtil.randomSeq(10)"type="sync"> </a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="inbound.stream.kcp.seed"></a-input>
|
||||
</a-form-item>
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='MTU'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.mtu" :min="576" :max="1460"></a-input-number>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.mtu" :min="576"
|
||||
:max="1460"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='TTI (ms)'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.tti" :min="10" :max="100"></a-input-number>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.tti" :min="10"
|
||||
:max="100"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Uplink (MB/s)'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.upCap" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.upCap"
|
||||
:min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Downlink (MB/s)'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.downCap" :min="0"></a-input-number>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.downCap"
|
||||
:min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Congestion'>
|
||||
<a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='Read Buffer (MB)'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.readBuffer" :min="0"></a-input-number>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.readBuffer"
|
||||
:min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Write Buffer (MB)'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.writeBuffer" :min="0"></a-input-number>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.writeBuffer"
|
||||
:min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
{{define "form/streamSettings"}}
|
||||
<!-- select stream network -->
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "transmission" }}'>
|
||||
<a-select v-model="inbound.stream.network" :style="{ width: '75%' }" @change="streamNetworkChange"
|
||||
<a-select v-model="inbound.stream.network" :style="{ width: '75%' }"
|
||||
@change="streamNetworkChange"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="tcp">TCP (RAW)</a-select-option>
|
||||
<a-select-option value="kcp">mKCP</a-select-option>
|
||||
|
|
@ -48,4 +50,10 @@
|
|||
<template>
|
||||
{{template "form/streamSockopt"}}
|
||||
</template>
|
||||
|
||||
<!-- finalmask - only for TCP, WS, HTTPUpgrade, XHTTP, mKCP -->
|
||||
<template
|
||||
v-if="['tcp', 'ws', 'httpupgrade', 'xhttp', 'kcp'].includes(inbound.stream.network)">
|
||||
{{template "form/streamFinalMask"}}
|
||||
</template>
|
||||
{{end}}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{{define "form/streamXHTTP"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model.trim="inbound.stream.xhttp.host"></a-input>
|
||||
</a-form-item>
|
||||
|
|
@ -7,38 +8,138 @@
|
|||
<a-input v-model.trim="inbound.stream.xhttp.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
|
||||
<a-button icon="plus" size="small" @click="inbound.stream.xhttp.addHeader('', '')"></a-button>
|
||||
<a-button icon="plus" size="small"
|
||||
@click="inbound.stream.xhttp.addHeader('', '')"></a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.xhttp.headers">
|
||||
<a-input-group compact
|
||||
v-for="(header, index) in inbound.stream.xhttp.headers">
|
||||
<a-input :style="{ width: '50%' }" v-model.trim="header.name"
|
||||
placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
|
||||
<template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
|
||||
<template slot="addonBefore" :style="{ margin: '0' }">[[ index+1
|
||||
]]</template>
|
||||
</a-input>
|
||||
<a-input :style="{ width: '50%' }" v-model.trim="header.value"
|
||||
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.xhttp.removeHeader(index)"></a-button>
|
||||
<a-button icon="minus" slot="addonAfter" size="small"
|
||||
@click="inbound.stream.xhttp.removeHeader(index)"></a-button>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item label='Mode'>
|
||||
<a-select v-model="inbound.stream.xhttp.mode" :style="{ width: '50%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in MODE_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
<a-select-option v-for="key in MODE_OPTION" :value="key">[[ key
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Buffered Upload" v-if="inbound.stream.xhttp.mode === 'packet-up'">
|
||||
<a-input-number v-model.number="inbound.stream.xhttp.scMaxBufferedPosts"></a-input-number>
|
||||
<a-form-item label="Max Buffered Upload"
|
||||
v-if="inbound.stream.xhttp.mode === 'packet-up'">
|
||||
<a-input-number
|
||||
v-model.number="inbound.stream.xhttp.scMaxBufferedPosts"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Upload Size (Byte)" v-if="inbound.stream.xhttp.mode === 'packet-up'">
|
||||
<a-input v-model.trim="inbound.stream.xhttp.scMaxEachPostBytes"></a-input>
|
||||
<a-form-item label="Max Upload Size (Byte)"
|
||||
v-if="inbound.stream.xhttp.mode === 'packet-up'">
|
||||
<a-input
|
||||
v-model.trim="inbound.stream.xhttp.scMaxEachPostBytes"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Stream-Up Server" v-if="inbound.stream.xhttp.mode === 'stream-up'">
|
||||
<a-input v-model.trim="inbound.stream.xhttp.scStreamUpServerSecs"></a-input>
|
||||
<a-form-item label="Stream-Up Server"
|
||||
v-if="inbound.stream.xhttp.mode === 'stream-up'">
|
||||
<a-input
|
||||
v-model.trim="inbound.stream.xhttp.scStreamUpServerSecs"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Padding Bytes">
|
||||
<a-input v-model.trim="inbound.stream.xhttp.xPaddingBytes"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Padding Obfs Mode">
|
||||
<a-switch v-model="inbound.stream.xhttp.xPaddingObfsMode"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="inbound.stream.xhttp.xPaddingObfsMode">
|
||||
<a-form-item label="Padding Key">
|
||||
<a-input v-model.trim="inbound.stream.xhttp.xPaddingKey"
|
||||
placeholder="x_padding"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Padding Header">
|
||||
<a-input v-model.trim="inbound.stream.xhttp.xPaddingHeader"
|
||||
placeholder="X-Padding"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Padding Placement">
|
||||
<a-select v-model="inbound.stream.xhttp.xPaddingPlacement"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Default (queryInHeader)</a-select-option>
|
||||
<a-select-option
|
||||
value="queryInHeader">queryInHeader</a-select-option>
|
||||
<a-select-option value="header">header</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Padding Method">
|
||||
<a-select v-model="inbound.stream.xhttp.xPaddingMethod"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Default (repeat-x)</a-select-option>
|
||||
<a-select-option value="repeat-x">repeat-x</a-select-option>
|
||||
<a-select-option value="tokenish">tokenish</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label="Uplink HTTP Method">
|
||||
<a-select v-model="inbound.stream.xhttp.uplinkHTTPMethod"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Default (POST)</a-select-option>
|
||||
<a-select-option value="POST">POST</a-select-option>
|
||||
<a-select-option value="PUT">PUT</a-select-option>
|
||||
<a-select-option value="GET">GET (packet-up only)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Session Placement">
|
||||
<a-select v-model="inbound.stream.xhttp.sessionPlacement"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Default (path)</a-select-option>
|
||||
<a-select-option value="path">path</a-select-option>
|
||||
<a-select-option value="header">header</a-select-option>
|
||||
<a-select-option value="cookie">cookie</a-select-option>
|
||||
<a-select-option value="query">query</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Session Key"
|
||||
v-if="inbound.stream.xhttp.sessionPlacement && inbound.stream.xhttp.sessionPlacement !== 'path'">
|
||||
<a-input v-model.trim="inbound.stream.xhttp.sessionKey"
|
||||
placeholder="x_session"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Sequence Placement">
|
||||
<a-select v-model="inbound.stream.xhttp.seqPlacement"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Default (path)</a-select-option>
|
||||
<a-select-option value="path">path</a-select-option>
|
||||
<a-select-option value="header">header</a-select-option>
|
||||
<a-select-option value="cookie">cookie</a-select-option>
|
||||
<a-select-option value="query">query</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Sequence Key"
|
||||
v-if="inbound.stream.xhttp.seqPlacement && inbound.stream.xhttp.seqPlacement !== 'path'">
|
||||
<a-input v-model.trim="inbound.stream.xhttp.seqKey"
|
||||
placeholder="x_seq"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Uplink Data Placement"
|
||||
v-if="inbound.stream.xhttp.mode === 'packet-up'">
|
||||
<a-select v-model="inbound.stream.xhttp.uplinkDataPlacement"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Default (body)</a-select-option>
|
||||
<a-select-option value="body">body</a-select-option>
|
||||
<a-select-option value="header">header</a-select-option>
|
||||
<a-select-option value="query">query</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Uplink Data Key"
|
||||
v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'">
|
||||
<a-input v-model.trim="inbound.stream.xhttp.uplinkDataKey"
|
||||
placeholder="x_data"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Uplink Chunk Size"
|
||||
v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'">
|
||||
<a-input-number v-model.number="inbound.stream.xhttp.uplinkChunkSize"
|
||||
:min="0" placeholder="0 (unlimited)"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="No SSE Header">
|
||||
<a-switch v-model="inbound.stream.xhttp.noSSEHeader"></a-switch>
|
||||
</a-form-item>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
{{define "form/tlsSettings"}}
|
||||
<!-- tls enable -->
|
||||
<a-form v-if="inbound.canEnableTls()" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form v-if="inbound.canEnableTls()" :colon="false"
|
||||
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider :style="{ margin: '3px 0' }"></a-divider>
|
||||
<a-form-item label='{{ i18n "security" }}'>
|
||||
<a-radio-group v-model="inbound.stream.security" button-style="solid">
|
||||
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
|
||||
<a-radio-button v-if="inbound.canEnableReality()" value="reality">Reality</a-radio-button>
|
||||
<a-radio-button v-if="inbound.canEnableReality()"
|
||||
value="reality">Reality</a-radio-button>
|
||||
<a-radio-button value="tls">TLS</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
|
@ -16,38 +18,46 @@
|
|||
<a-input v-model.trim="inbound.stream.tls.sni"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Cipher Suites">
|
||||
<a-select v-model="inbound.stream.tls.cipherSuites" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="">Auto</a-select-option>
|
||||
<a-select-option v-for="key,value in TLS_CIPHER_OPTION" :value="key">[[ value ]]</a-select-option>
|
||||
<a-select v-model="inbound.stream.tls.cipherSuites"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Auto</a-select-option>
|
||||
<a-select-option v-for="key,value in TLS_CIPHER_OPTION" :value="key">[[
|
||||
value ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Min/Max Version">
|
||||
<a-input-group compact>
|
||||
<a-select v-model="inbound.stream.tls.minVersion" :style="{ width: '50%' }"
|
||||
<a-select v-model="inbound.stream.tls.minVersion"
|
||||
:style="{ width: '50%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model="inbound.stream.tls.maxVersion" :style="{ width: '50%' }"
|
||||
<a-select v-model="inbound.stream.tls.maxVersion"
|
||||
:style="{ width: '50%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model="inbound.stream.tls.settings.fingerprint" :style="{ width: '100%' }"
|
||||
<a-select v-model="inbound.stream.tls.settings.fingerprint"
|
||||
:style="{ width: '100%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value=''>None</a-select-option>
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
<a-select-option value>None</a-select-option>
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="ALPN">
|
||||
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" v-model="inbound.stream.tls.alpn">
|
||||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
|
||||
<a-select mode="multiple"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="inbound.stream.tls.alpn">
|
||||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Allow Insecure">
|
||||
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Reject Unknown SNI">
|
||||
<a-switch v-model="inbound.stream.tls.rejectUnknownSni"></a-switch>
|
||||
</a-form-item>
|
||||
|
|
@ -57,21 +67,25 @@
|
|||
<a-form-item label="Session Resumption">
|
||||
<a-switch v-model="inbound.stream.tls.enableSessionResumption"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="VerifyPeerCertInNames">
|
||||
<a-input v-model.trim="inbound.stream.tls.verifyPeerCertInNames"></a-input>
|
||||
</a-form-item>
|
||||
<a-divider :style="{ margin: '3px 0' }"></a-divider>
|
||||
<template v-for="cert,index in inbound.stream.tls.certs">
|
||||
<a-form-item label='{{ i18n "certificate" }}'>
|
||||
<a-radio-group v-model="cert.useFile" button-style="solid" :style="{ display: 'inline-flex', whiteSpace: 'nowrap', maxWidth: '100%' }">
|
||||
<a-radio-button :value="true" :style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
|
||||
<a-radio-button :value="false" :style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
|
||||
<a-radio-group v-model="cert.useFile" button-style="solid"
|
||||
:style="{ display: 'inline-flex', whiteSpace: 'nowrap', maxWidth: '100%' }">
|
||||
<a-radio-button :value="true"
|
||||
:style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{
|
||||
i18n "pages.inbounds.certificatePath" }}</a-radio-button>
|
||||
<a-radio-button :value="false"
|
||||
:style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{
|
||||
i18n "pages.inbounds.certificateContent" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button icon="plus" v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()"></a-button>
|
||||
<a-button icon="minus" v-if="inbound.stream.tls.certs.length>1" type="primary" size="small"
|
||||
<a-button icon="plus" v-if="index === 0" type="primary" size="small"
|
||||
@click="inbound.stream.tls.addCert()"></a-button>
|
||||
<a-button icon="minus" v-if="inbound.stream.tls.certs.length>1"
|
||||
type="primary" size="small"
|
||||
@click="inbound.stream.tls.removeCert(index)"></a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
|
|
@ -83,7 +97,8 @@
|
|||
<a-input v-model.trim="cert.keyFile"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">
|
||||
<a-button type="primary" icon="import"
|
||||
@click="setDefaultCertData(index)">
|
||||
{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
|
@ -99,8 +114,10 @@
|
|||
<a-switch v-model="cert.oneTimeLoading"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='Usage Option'>
|
||||
<a-select v-model="cert.usage" :style="{ width: '50%' }" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in USAGE_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
<a-select v-model="cert.usage" :style="{ width: '50%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in USAGE_OPTION" :value="key">[[ key
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Build Chain" v-if="cert.usage === 'issue'">
|
||||
|
|
@ -108,20 +125,22 @@
|
|||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label='ECH key'>
|
||||
<a-input v-model="inbound.stream.tls.echServerKeys"></a-input>
|
||||
<a-input v-model="inbound.stream.tls.echServerKeys"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='ECH config'>
|
||||
<a-input v-model="inbound.stream.tls.settings.echConfigList"></a-input>
|
||||
<a-input v-model="inbound.stream.tls.settings.echConfigList"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='ECH force query'>
|
||||
<a-select v-model="inbound.stream.tls.echForceQuery"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in ['none', 'half', 'full']" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model="inbound.stream.tls.echForceQuery"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in ['none', 'half', 'full']" :value="key">[[
|
||||
key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button type="primary" icon="import" @click="getNewEchCert">Get New ECH Cert</a-button>
|
||||
<a-button type="primary" icon="import" @click="getNewEchCert">Get New
|
||||
ECH Cert</a-button>
|
||||
<a-button danger @click="clearEchCert">Clear</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
|
|
|
|||
|
|
@ -1602,41 +1602,42 @@
|
|||
if (payload && Array.isArray(payload)) {
|
||||
// Use setInbounds to properly convert to DBInbound objects with methods
|
||||
this.setInbounds(payload);
|
||||
this.searchInbounds(this.searchKey);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for traffic updates
|
||||
window.wsClient.on('traffic', (payload) => {
|
||||
if (payload && payload.clientTraffics && Array.isArray(payload.clientTraffics)) {
|
||||
// Update client traffic statistics
|
||||
payload.clientTraffics.forEach(clientTraffic => {
|
||||
const dbInbound = this.dbInbounds.find(ib => {
|
||||
if (!ib) return false;
|
||||
const clients = this.getInboundClients(ib);
|
||||
return clients && Array.isArray(clients) && clients.some(c => c && c.email === clientTraffic.email);
|
||||
});
|
||||
if (dbInbound && dbInbound.clientStats && Array.isArray(dbInbound.clientStats)) {
|
||||
const stats = dbInbound.clientStats.find(s => s && s.email === clientTraffic.email);
|
||||
if (stats) {
|
||||
stats.up = clientTraffic.up || stats.up;
|
||||
stats.down = clientTraffic.down || stats.down;
|
||||
stats.total = clientTraffic.total || stats.total;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Note: Do NOT update total consumed traffic (stats.up, stats.down) from this event
|
||||
// because clientTraffics contains delta/incremental values, not total accumulated values.
|
||||
// Total traffic is updated via the 'inbounds' event which contains accumulated values from database.
|
||||
|
||||
// Update online clients list in real-time
|
||||
if (payload && Array.isArray(payload.onlineClients)) {
|
||||
this.onlineClients = payload.onlineClients;
|
||||
// Recalculate client counts to update online status
|
||||
this.dbInbounds.forEach(dbInbound => {
|
||||
const inbound = this.inbounds.find(ib => ib.id === dbInbound.id);
|
||||
if (inbound && this.clientCount[dbInbound.id]) {
|
||||
this.clientCount[dbInbound.id] = this.getClientCounts(dbInbound, inbound);
|
||||
const nextOnlineClients = payload.onlineClients;
|
||||
let onlineChanged = this.onlineClients.length !== nextOnlineClients.length;
|
||||
if (!onlineChanged) {
|
||||
const prevSet = new Set(this.onlineClients);
|
||||
for (const email of nextOnlineClients) {
|
||||
if (!prevSet.has(email)) {
|
||||
onlineChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
this.onlineClients = nextOnlineClients;
|
||||
if (onlineChanged) {
|
||||
// Recalculate client counts to update online status
|
||||
this.dbInbounds.forEach(dbInbound => {
|
||||
const inbound = this.inbounds.find(ib => ib.id === dbInbound.id);
|
||||
if (inbound && this.clientCount[dbInbound.id]) {
|
||||
this.clientCount[dbInbound.id] = this.getClientCounts(dbInbound, inbound);
|
||||
}
|
||||
});
|
||||
|
||||
if (this.enableFilter) {
|
||||
this.filterInbounds();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update last online map in real-time
|
||||
|
|
@ -1645,8 +1646,6 @@
|
|||
}
|
||||
});
|
||||
|
||||
// Notifications disabled - white notifications are not needed
|
||||
|
||||
// Fallback to polling if WebSocket fails
|
||||
window.wsClient.on('error', () => {
|
||||
console.warn('WebSocket connection failed, falling back to polling');
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -219,14 +219,14 @@
|
|||
rule = {};
|
||||
newRule = {};
|
||||
rule.type = "field";
|
||||
rule.domain = value.domain.length > 0 ? value.domain.split(',') : [];
|
||||
rule.ip = value.ip.length > 0 ? value.ip.split(',') : [];
|
||||
rule.domain = value.domain.length > 0 ? value.domain.split(',').map(s => s.trim()) : [];
|
||||
rule.ip = value.ip.length > 0 ? value.ip.split(',').map(s => s.trim()) : [];
|
||||
rule.port = value.port;
|
||||
rule.sourcePort = value.sourcePort;
|
||||
rule.vlessRoute = value.vlessRoute;
|
||||
rule.network = value.network;
|
||||
rule.sourceIP = value.sourceIP.length > 0 ? value.sourceIP.split(',') : [];
|
||||
rule.user = value.user.length > 0 ? value.user.split(',') : [];
|
||||
rule.sourceIP = value.sourceIP.length > 0 ? value.sourceIP.split(',').map(s => s.trim()) : [];
|
||||
rule.user = value.user.length > 0 ? value.user.split(',').map(s => s.trim()) : [];
|
||||
rule.inboundTag = value.inboundTag;
|
||||
rule.protocol = value.protocol;
|
||||
rule.attrs = Object.fromEntries(value.attrs);
|
||||
|
|
|
|||
|
|
@ -15,13 +15,6 @@
|
|||
<a-switch v-model="allSetting.subJsonEnable"></a-switch>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.settings.subTitle"}}</template>
|
||||
<template #description>{{ i18n "pages.settings.subTitleDesc"}}</template>
|
||||
<template #control>
|
||||
<a-input type="text" v-model="allSetting.subTitle"></a-input>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.settings.subListen"}}</template>
|
||||
<template #description>{{ i18n "pages.settings.subListenDesc"}}</template>
|
||||
|
|
@ -78,6 +71,50 @@
|
|||
<a-switch v-model="allSetting.subShowInfo"></a-switch>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-divider>{{ i18n "pages.xray.basicTemplate"}}</a-divider>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.settings.subTitle"}}</template>
|
||||
<template #description>{{ i18n "pages.settings.subTitleDesc"}}</template>
|
||||
<template #control>
|
||||
<a-input type="text" v-model="allSetting.subTitle"></a-input>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.settings.subSupportUrl"}}</template>
|
||||
<template #description>{{ i18n "pages.settings.subSupportUrlDesc"}}</template>
|
||||
<template #control>
|
||||
<a-input type="text" v-model="allSetting.subSupportUrl" placeholder="https://example.com"></a-input>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.settings.subProfileUrl"}}</template>
|
||||
<template #description>{{ i18n "pages.settings.subProfileUrlDesc"}}</template>
|
||||
<template #control>
|
||||
<a-input type="text" v-model="allSetting.subProfileUrl" placeholder="https://example.com"></a-input>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.settings.subAnnounce"}}</template>
|
||||
<template #description>{{ i18n "pages.settings.subAnnounceDesc"}}</template>
|
||||
<template #control>
|
||||
<a-textarea v-model="allSetting.subAnnounce"></a-textarea>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-divider>{{ i18n "pages.xray.advancedTemplate"}} (Happ)</a-divider>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.settings.subEnableRouting"}}</template>
|
||||
<template #description>{{ i18n "pages.settings.subEnableRoutingDesc"}}</template>
|
||||
<template #control>
|
||||
<a-switch v-model="allSetting.subEnableRouting"></a-switch>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.settings.subRoutingRules"}}</template>
|
||||
<template #description>{{ i18n "pages.settings.subRoutingRulesDesc"}}</template>
|
||||
<template #control>
|
||||
<a-textarea v-model="allSetting.subRoutingRules" placeholder="happ://routing/add/..."></a-textarea>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel key="3" header='{{ i18n "pages.settings.certs" }}'>
|
||||
<a-setting-list-item paddings="small">
|
||||
|
|
|
|||
|
|
@ -5,6 +5,43 @@
|
|||
<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
|
||||
<style>
|
||||
.subscription-page .subscription-link-box {
|
||||
cursor: pointer;
|
||||
border-radius: 12px;
|
||||
padding: 25px 20px 15px 20px;
|
||||
margin-top: -12px;
|
||||
word-break: break-all;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
text-align: left;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dark.subscription-page .subscription-link-box {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dark.subscription-page .subscription-link-box:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.light.subscription-page .subscription-link-box {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.light.subscription-page .subscription-link-box:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-color: rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
</style>
|
||||
{{ template "page/head_end" .}}
|
||||
|
||||
{{ template "page/body_start" .}}
|
||||
|
|
@ -20,28 +57,20 @@
|
|||
</a-space>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-popover
|
||||
:overlay-class-name="themeSwitcher.currentTheme"
|
||||
title='{{ i18n "menu.settings" }}'
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}'
|
||||
placement="bottomRight" trigger="click">
|
||||
<template #content>
|
||||
<a-space direction="vertical" :size="10">
|
||||
<a-theme-switch-login></a-theme-switch-login>
|
||||
<span>{{ i18n "pages.settings.language"
|
||||
}}</span>
|
||||
<a-select ref="selectLang" class="w-100"
|
||||
v-model="lang"
|
||||
<a-select ref="selectLang" class="w-100" v-model="lang"
|
||||
@change="LanguageManager.setLanguage(lang)"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="l.value"
|
||||
label="English"
|
||||
v-for="l in LanguageManager.supportedLanguages"
|
||||
:key="l.value">
|
||||
<span role="img"
|
||||
:aria-label="l.name"
|
||||
v-text="l.icon"></span>
|
||||
<span
|
||||
v-text="l.name"></span>
|
||||
<a-select-option :value="l.value" label="English"
|
||||
v-for="l in LanguageManager.supportedLanguages" :key="l.value">
|
||||
<span role="img" :aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
|
|
@ -53,42 +82,31 @@
|
|||
<a-form layout="vertical">
|
||||
<a-form-item>
|
||||
<a-space direction="vertical" align="center">
|
||||
<a-row type="flex" :gutter="[8,8]"
|
||||
justify="center" style="width:100%">
|
||||
<a-col :xs="24" :sm="app.subJsonUrl ? 12 : 24"
|
||||
style="text-align:center;">
|
||||
<a-row type="flex" :gutter="[8,8]" justify="center" style="width:100%">
|
||||
<a-col :xs="24" :sm="app.subJsonUrl ? 12 : 24" style="text-align:center;">
|
||||
<tr-qr-box class="qr-box">
|
||||
<a-tag color="purple"
|
||||
class="qr-tag">
|
||||
<a-tag color="purple" class="qr-tag">
|
||||
<span>{{ i18n
|
||||
"pages.settings.subSettings"}}</span>
|
||||
</a-tag>
|
||||
<tr-qr-bg class="qr-bg-sub">
|
||||
<tr-qr-bg-inner
|
||||
class="qr-bg-sub-inner">
|
||||
<canvas id="qrcode"
|
||||
class="qr-cv"
|
||||
title='{{ i18n "copy" }}'
|
||||
<tr-qr-bg-inner class="qr-bg-sub-inner">
|
||||
<canvas id="qrcode" class="qr-cv" title='{{ i18n "copy" }}'
|
||||
@click="copy(app.subUrl)"></canvas>
|
||||
</tr-qr-bg-inner>
|
||||
</tr-qr-bg>
|
||||
</tr-qr-box>
|
||||
</a-col>
|
||||
<a-col v-if="app.subJsonUrl" :xs="24" :sm="12"
|
||||
style="text-align:center;">
|
||||
<a-col v-if="app.subJsonUrl" :xs="24" :sm="12" style="text-align:center;">
|
||||
<tr-qr-box class="qr-box">
|
||||
<a-tag color="purple"
|
||||
class="qr-tag">
|
||||
<a-tag color="purple" class="qr-tag">
|
||||
<span>{{ i18n
|
||||
"pages.settings.subSettings"}}
|
||||
Json</span>
|
||||
</a-tag>
|
||||
<tr-qr-bg class="qr-bg-sub">
|
||||
<tr-qr-bg-inner
|
||||
class="qr-bg-sub-inner">
|
||||
<canvas id="qrcode-subjson"
|
||||
class="qr-cv"
|
||||
title='{{ i18n "copy" }}'
|
||||
<tr-qr-bg-inner class="qr-bg-sub-inner">
|
||||
<canvas id="qrcode-subjson" class="qr-cv" title='{{ i18n "copy" }}'
|
||||
@click="copy(app.subJsonUrl)"></canvas>
|
||||
</tr-qr-bg-inner>
|
||||
</tr-qr-bg>
|
||||
|
|
@ -100,45 +118,36 @@
|
|||
|
||||
<a-form-item>
|
||||
<a-descriptions bordered :column="1" size="small">
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "subscription.subId" }}'>[[
|
||||
<a-descriptions-item label='{{ i18n "subscription.subId" }}'>[[
|
||||
app.sId
|
||||
]]</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "subscription.status" }}'>
|
||||
<a-descriptions-item label='{{ i18n "subscription.status" }}'>
|
||||
<template v-if="isUnlimited">
|
||||
<a-tag color="purple">{{ i18n
|
||||
"subscription.unlimited" }}</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag
|
||||
:color="isActive ? 'green' : 'red'">[[
|
||||
<a-tag :color="isActive ? 'green' : 'red'">[[
|
||||
isActive ? '{{ i18n
|
||||
"subscription.active" }}' : '{{ i18n
|
||||
"subscription.inactive" }}'
|
||||
]]</a-tag>
|
||||
</template>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "subscription.downloaded" }}'>[[
|
||||
<a-descriptions-item label='{{ i18n "subscription.downloaded" }}'>[[
|
||||
app.download
|
||||
]]</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "subscription.uploaded" }}'>[[
|
||||
<a-descriptions-item label='{{ i18n "subscription.uploaded" }}'>[[
|
||||
app.upload
|
||||
]]</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "usage" }}'>[[ app.used
|
||||
<a-descriptions-item label='{{ i18n "usage" }}'>[[ app.used
|
||||
]]</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "subscription.totalQuota" }}'>[[
|
||||
<a-descriptions-item label='{{ i18n "subscription.totalQuota" }}'>[[
|
||||
app.total
|
||||
]]</a-descriptions-item>
|
||||
<a-descriptions-item v-if="app.totalByte > 0"
|
||||
label='{{ i18n "remained" }}'>[[
|
||||
<a-descriptions-item v-if="app.totalByte > 0" label='{{ i18n "remained" }}'>[[
|
||||
app.remained ]]</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "lastOnline" }}'>
|
||||
<a-descriptions-item label='{{ i18n "lastOnline" }}'>
|
||||
<template v-if="app.lastOnlineMs > 0">
|
||||
[[ IntlUtil.formatDate(app.lastOnlineMs) ]]
|
||||
</template>
|
||||
|
|
@ -146,8 +155,7 @@
|
|||
<span>-</span>
|
||||
</template>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "subscription.expiry" }}'>
|
||||
<a-descriptions-item label='{{ i18n "subscription.expiry" }}'>
|
||||
<template v-if="app.expireMs === 0">
|
||||
{{ i18n "subscription.noExpiry" }}
|
||||
</template>
|
||||
|
|
@ -160,32 +168,33 @@
|
|||
</a-form>
|
||||
|
||||
<br />
|
||||
<a-list bordered>
|
||||
<a-list-item v-for="(link, idx) in links" :key="link">
|
||||
<div style="width:100%; text-align:center;">
|
||||
<a-button type="primary" :block="isMobile"
|
||||
@click="copy(link)">[[ linkName(link, idx)
|
||||
]]</a-button>
|
||||
<div v-for="(link, idx) in links" :key="link"
|
||||
style="position: relative; margin-bottom: 20px; text-align: center;">
|
||||
<div class="qr-box" style="display: inline-block; width: 100%; max-width: 100%;">
|
||||
<a-tag color="purple"
|
||||
style="margin-bottom: -10px; position: relative; z-index: 2; box-shadow: 0 2px 4px rgba(0,0,0,0.2);">
|
||||
<span>[[ linkName(link, idx) ]]</span>
|
||||
</a-tag>
|
||||
<div @click="copy(link)" class="subscription-link-box">
|
||||
[[ link ]]
|
||||
</div>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<a-form layout="vertical">
|
||||
<a-form-item>
|
||||
<a-row type="flex" justify="center" :gutter="[8,8]"
|
||||
style="width:100%">
|
||||
<a-col :xs="24" :sm="12"
|
||||
style="text-align:center;">
|
||||
<a-row type="flex" justify="center" :gutter="[8,8]" style="width:100%">
|
||||
<a-col :xs="24" :sm="12" style="text-align:center;">
|
||||
<!-- Android dropdown -->
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button icon="android" :block="isMobile"
|
||||
:style="{ marginTop: isMobile ? '6px' : 0 }"
|
||||
size="large" type="primary">
|
||||
:style="{ marginTop: isMobile ? '6px' : 0 }" size="large" type="primary">
|
||||
Android <a-icon type="down" />
|
||||
</a-button>
|
||||
<a-menu slot="overlay"
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<a-menu slot="overlay" :class="themeSwitcher.currentTheme">
|
||||
<a-menu-item key="android-v2box"
|
||||
@click="open('v2box://install-sub?url=' + encodeURIComponent(app.subUrl) + '&name=' + encodeURIComponent(app.sId))">V2Box</a-menu-item>
|
||||
<a-menu-item key="android-v2rayng"
|
||||
|
|
@ -194,39 +203,32 @@
|
|||
@click="copy(app.subUrl)">Sing-box</a-menu-item>
|
||||
<a-menu-item key="android-v2raytun"
|
||||
@click="copy(app.subUrl)">V2RayTun</a-menu-item>
|
||||
<a-menu-item key="android-npvtunnel"
|
||||
@click="copy(app.subUrl)">NPV
|
||||
<a-menu-item key="android-npvtunnel" @click="copy(app.subUrl)">NPV
|
||||
Tunnel</a-menu-item>
|
||||
<a-menu-item key="android-happ"
|
||||
@click="open('happ://add/' + encodeURIComponent(app.subUrl))">Happ</a-menu-item>
|
||||
<a-menu-item key="android-happ"
|
||||
@click="open('happ://add/' + encodeURIComponent(app.subUrl))">Happ</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12"
|
||||
style="text-align:center;">
|
||||
<a-col :xs="24" :sm="12" style="text-align:center;">
|
||||
<!-- iOS dropdown -->
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button icon="apple" :block="isMobile"
|
||||
:style="{ marginTop: isMobile ? '6px' : 0 }"
|
||||
size="large" type="primary">
|
||||
:style="{ marginTop: isMobile ? '6px' : 0 }" size="large" type="primary">
|
||||
iOS <a-icon type="down" />
|
||||
</a-button>
|
||||
<a-menu slot="overlay"
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<a-menu slot="overlay" :class="themeSwitcher.currentTheme">
|
||||
<a-menu-item key="ios-shadowrocket"
|
||||
@click="open(shadowrocketUrl)">Shadowrocket</a-menu-item>
|
||||
<a-menu-item key="ios-v2box"
|
||||
@click="open(v2boxUrl)">V2Box</a-menu-item>
|
||||
<a-menu-item key="ios-v2box" @click="open(v2boxUrl)">V2Box</a-menu-item>
|
||||
<a-menu-item key="ios-streisand"
|
||||
@click="open(streisandUrl)">Streisand</a-menu-item>
|
||||
<a-menu-item key="ios-v2raytun"
|
||||
@click="copy(v2raytunUrl)">V2RayTun</a-menu-item>
|
||||
<a-menu-item key="ios-npvtunnel"
|
||||
@click="copy(npvtunUrl)">NPV
|
||||
<a-menu-item key="ios-npvtunnel" @click="copy(npvtunUrl)">NPV
|
||||
Tunnel
|
||||
</a-menu-item>
|
||||
<a-menu-item key="ios-happ"
|
||||
@click="open(happUrl)">Happ</a-menu-item>
|
||||
<a-menu-item key="ios-happ" @click="open(happUrl)">Happ</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</a-col>
|
||||
|
|
@ -240,17 +242,12 @@
|
|||
</a-layout>
|
||||
|
||||
<!-- Bootstrap data for external JS -->
|
||||
<template id="subscription-data" data-sid="{{ .sId }}"
|
||||
data-sub-url="{{ .subUrl }}" data-subjson-url="{{ .subJsonUrl }}"
|
||||
data-download="{{ .download }}"
|
||||
data-upload="{{ .upload }}" data-used="{{ .used }}"
|
||||
data-total="{{ .total }}" data-remained="{{ .remained }}"
|
||||
data-expire="{{ .expire }}" data-lastonline="{{ .lastOnline }}"
|
||||
data-downloadbyte="{{ .downloadByte }}"
|
||||
data-uploadbyte="{{ .uploadByte }}" data-totalbyte="{{ .totalByte }}"
|
||||
<template id="subscription-data" data-sid="{{ .sId }}" data-sub-url="{{ .subUrl }}" data-subjson-url="{{ .subJsonUrl }}"
|
||||
data-download="{{ .download }}" data-upload="{{ .upload }}" data-used="{{ .used }}" data-total="{{ .total }}"
|
||||
data-remained="{{ .remained }}" data-expire="{{ .expire }}" data-lastonline="{{ .lastOnline }}"
|
||||
data-downloadbyte="{{ .downloadByte }}" data-uploadbyte="{{ .uploadByte }}" data-totalbyte="{{ .totalByte }}"
|
||||
data-datepicker="{{ .datepicker }}"></template>
|
||||
<textarea id="subscription-links"
|
||||
style="display:none">{{ range .result }}{{ . }}
|
||||
<textarea id="subscription-links" style="display:none">{{ range .result }}{{ . }}
|
||||
{{ end }}</textarea>
|
||||
|
||||
{{template "component/aThemeSwitch" .}}
|
||||
|
|
|
|||
|
|
@ -4,18 +4,22 @@
|
|||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<a-alert type="warning" :style="{ textAlign: 'center' }">
|
||||
<template slot="message">
|
||||
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
|
||||
<a-icon type="exclamation-circle" theme="filled"
|
||||
:style="{ color: '#FFA031' }"></a-icon>
|
||||
<span>{{ i18n "pages.xray.generalConfigsDesc" }}</span>
|
||||
</template>
|
||||
</a-alert>
|
||||
</a-row>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.xray.FreedomStrategy" }}</template>
|
||||
<template #description>{{ i18n "pages.xray.FreedomStrategyDesc" }}</template>
|
||||
<template #description>{{ i18n "pages.xray.FreedomStrategyDesc"
|
||||
}}</template>
|
||||
<template #control>
|
||||
<a-select v-model="freedomStrategy" :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
<a-select v-model="freedomStrategy"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
:style="{ width: '100%' }">
|
||||
<a-select-option v-for="s in OutboundDomainStrategies" :value="s">
|
||||
<a-select-option v-for="s in OutboundDomainStrategies"
|
||||
:value="s">
|
||||
<span>[[ s ]]</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
|
|
@ -23,42 +27,63 @@
|
|||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.xray.RoutingStrategy" }}</template>
|
||||
<template #description>{{ i18n "pages.xray.RoutingStrategyDesc" }}</template>
|
||||
<template #description>{{ i18n "pages.xray.RoutingStrategyDesc"
|
||||
}}</template>
|
||||
<template #control>
|
||||
<a-select v-model="routingStrategy" :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
<a-select v-model="routingStrategy"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
:style="{ width: '100%' }">
|
||||
<a-select-option v-for="s in routingDomainStrategies" :value="s">
|
||||
<a-select-option v-for="s in routingDomainStrategies"
|
||||
:value="s">
|
||||
<span>[[ s ]]</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</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 key="2" header='{{ i18n "pages.xray.statistics" }}'>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.xray.statsInboundUplink" }}</template>
|
||||
<template #description>{{ i18n "pages.xray.statsInboundUplinkDesc" }}</template>
|
||||
<template #title>{{ i18n "pages.xray.statsInboundUplink"
|
||||
}}</template>
|
||||
<template #description>{{ i18n "pages.xray.statsInboundUplinkDesc"
|
||||
}}</template>
|
||||
<template #control>
|
||||
<a-switch v-model="statsInboundUplink"></a-switch>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.xray.statsInboundDownlink" }}</template>
|
||||
<template #description>{{ i18n "pages.xray.statsInboundDownlinkDesc" }}</template>
|
||||
<template #title>{{ i18n "pages.xray.statsInboundDownlink"
|
||||
}}</template>
|
||||
<template #description>{{ i18n "pages.xray.statsInboundDownlinkDesc"
|
||||
}}</template>
|
||||
<template #control>
|
||||
<a-switch v-model="statsInboundDownlink"></a-switch>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.xray.statsOutboundUplink" }}</template>
|
||||
<template #description>{{ i18n "pages.xray.statsOutboundUplinkDesc" }}</template>
|
||||
<template #title>{{ i18n "pages.xray.statsOutboundUplink"
|
||||
}}</template>
|
||||
<template #description>{{ i18n "pages.xray.statsOutboundUplinkDesc"
|
||||
}}</template>
|
||||
<template #control>
|
||||
<a-switch v-model="statsOutboundUplink"></a-switch>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.xray.statsOutboundDownlink" }}</template>
|
||||
<template #description>{{ i18n "pages.xray.statsOutboundDownlinkDesc" }}</template>
|
||||
<template #title>{{ i18n "pages.xray.statsOutboundDownlink"
|
||||
}}</template>
|
||||
<template #description>{{ i18n
|
||||
"pages.xray.statsOutboundDownlinkDesc" }}</template>
|
||||
<template #control>
|
||||
<a-switch v-model="statsOutboundDownlink"></a-switch>
|
||||
</template>
|
||||
|
|
@ -68,16 +93,20 @@
|
|||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<a-alert type="warning" :style="{ textAlign: 'center' }">
|
||||
<template slot="message">
|
||||
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
|
||||
<a-icon type="exclamation-circle" theme="filled"
|
||||
:style="{ color: '#FFA031' }"></a-icon>
|
||||
<span>{{ i18n "pages.xray.logConfigsDesc" }}</span>
|
||||
</template>
|
||||
</a-alert>
|
||||
</a-row>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.xray.logLevel" }}</template>
|
||||
<template #description>{{ i18n "pages.xray.logLevelDesc" }}</template>
|
||||
<template #description>{{ i18n "pages.xray.logLevelDesc"
|
||||
}}</template>
|
||||
<template #control>
|
||||
<a-select v-model="logLevel" :dropdown-class-name="themeSwitcher.currentTheme" :style="{ width: '100%' }">
|
||||
<a-select v-model="logLevel"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
:style="{ width: '100%' }">
|
||||
<a-select-option v-for="s in log.loglevel" :value="s">
|
||||
<span>[[ s ]]</span>
|
||||
</a-select-option>
|
||||
|
|
@ -86,10 +115,13 @@
|
|||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.xray.accessLog" }}</template>
|
||||
<template #description>{{ i18n "pages.xray.accessLogDesc" }}</template>
|
||||
<template #description>{{ i18n "pages.xray.accessLogDesc"
|
||||
}}</template>
|
||||
<template #control>
|
||||
<a-select v-model="accessLog" :dropdown-class-name="themeSwitcher.currentTheme" :style="{ width: '100%' }">
|
||||
<a-select-option value=''>
|
||||
<a-select v-model="accessLog"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
:style="{ width: '100%' }">
|
||||
<a-select-option value>
|
||||
<span>Empty</span>
|
||||
</a-select-option>
|
||||
<a-select-option v-for="s in log.access" :value="s">
|
||||
|
|
@ -100,10 +132,13 @@
|
|||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.xray.errorLog" }}</template>
|
||||
<template #description>{{ i18n "pages.xray.errorLogDesc" }}</template>
|
||||
<template #description>{{ i18n "pages.xray.errorLogDesc"
|
||||
}}</template>
|
||||
<template #control>
|
||||
<a-select v-model="errorLog" :dropdown-class-name="themeSwitcher.currentTheme" :style="{ width: '100%' }">
|
||||
<a-select-option value=''>
|
||||
<a-select v-model="errorLog"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
:style="{ width: '100%' }">
|
||||
<a-select-option value>
|
||||
<span>Empty</span>
|
||||
</a-select-option>
|
||||
<a-select-option v-for="s in log.error" :value="s">
|
||||
|
|
@ -114,11 +149,13 @@
|
|||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.xray.maskAddress" }}</template>
|
||||
<template #description>{{ i18n "pages.xray.maskAddressDesc" }}</template>
|
||||
<template #description>{{ i18n "pages.xray.maskAddressDesc"
|
||||
}}</template>
|
||||
<template #control>
|
||||
<a-select v-model="maskAddressLog" :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
<a-select v-model="maskAddressLog"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
:style="{ width: '100%' }">
|
||||
<a-select-option value=''>
|
||||
<a-select-option value>
|
||||
<span>Empty</span>
|
||||
</a-select-option>
|
||||
<a-select-option v-for="s in log.maskAddress" :value="s">
|
||||
|
|
@ -139,7 +176,8 @@
|
|||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<a-alert type="warning" :style="{ textAlign: 'center' }">
|
||||
<template slot="message">
|
||||
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
|
||||
<a-icon type="exclamation-circle" theme="filled"
|
||||
:style="{ color: '#FFA031' }"></a-icon>
|
||||
<span>{{ i18n "pages.xray.blockConfigsDesc" }}</span>
|
||||
</template>
|
||||
</a-alert>
|
||||
|
|
@ -153,17 +191,21 @@
|
|||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<a-alert type="warning" :style="{ textAlign: 'center' }">
|
||||
<template slot="message">
|
||||
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
|
||||
<span>{{ i18n "pages.xray.blockConnectionsConfigsDesc" }}</span>
|
||||
<a-icon type="exclamation-circle" theme="filled"
|
||||
:style="{ color: '#FFA031' }"></a-icon>
|
||||
<span>{{ i18n "pages.xray.blockConnectionsConfigsDesc"
|
||||
}}</span>
|
||||
</template>
|
||||
</a-alert>
|
||||
</a-row>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.xray.blockips" }}</template>
|
||||
<template #control>
|
||||
<a-select mode="tags" v-model="blockedIPs" :style="{ width: '100%' }"
|
||||
<a-select mode="tags" v-model="blockedIPs"
|
||||
:style="{ width: '100%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.IPsOptions">
|
||||
<a-select-option :value="p.value" :label="p.label"
|
||||
v-for="p in settingsData.IPsOptions">
|
||||
<span>[[ p.label ]]</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
|
|
@ -172,28 +214,35 @@
|
|||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.xray.blockdomains" }}</template>
|
||||
<template #control>
|
||||
<a-select mode="tags" v-model="blockedDomains" :style="{ width: '100%' }"
|
||||
<a-select mode="tags" v-model="blockedDomains"
|
||||
:style="{ width: '100%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.BlockDomainsOptions">
|
||||
<a-select-option :value="p.value" :label="p.label"
|
||||
v-for="p in settingsData.BlockDomainsOptions">
|
||||
<span>[[ p.label ]]</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<a-alert type="warning" :style="{ textAlign: 'center', marginTop: '20px' }">
|
||||
<a-alert type="warning"
|
||||
:style="{ textAlign: 'center', marginTop: '20px' }">
|
||||
<template slot="message">
|
||||
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
|
||||
<span>{{ i18n "pages.xray.directConnectionsConfigsDesc" }}</span>
|
||||
<a-icon type="exclamation-circle" theme="filled"
|
||||
:style="{ color: '#FFA031' }"></a-icon>
|
||||
<span>{{ i18n "pages.xray.directConnectionsConfigsDesc"
|
||||
}}</span>
|
||||
</template>
|
||||
</a-alert>
|
||||
</a-row>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.xray.directips" }}</template>
|
||||
<template #control>
|
||||
<a-select mode="tags" :style="{ width: '100%' }" v-model="directIPs"
|
||||
<a-select mode="tags" :style="{ width: '100%' }"
|
||||
v-model="directIPs"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.IPsOptions">
|
||||
<a-select-option :value="p.value" :label="p.label"
|
||||
v-for="p in settingsData.IPsOptions">
|
||||
<span>[[ p.label ]]</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
|
|
@ -202,18 +251,22 @@
|
|||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.xray.directdomains" }}</template>
|
||||
<template #control>
|
||||
<a-select mode="tags" :style="{ width: '100%' }" v-model="directDomains"
|
||||
<a-select mode="tags" :style="{ width: '100%' }"
|
||||
v-model="directDomains"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.DomainsOptions">
|
||||
<a-select-option :value="p.value" :label="p.label"
|
||||
v-for="p in settingsData.DomainsOptions">
|
||||
<span>[[ p.label ]]</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<a-alert type="warning" :style="{ textAlign: 'center', marginTop: '20px' }">
|
||||
<a-alert type="warning"
|
||||
:style="{ textAlign: 'center', marginTop: '20px' }">
|
||||
<template slot="message">
|
||||
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
|
||||
<a-icon type="exclamation-circle" theme="filled"
|
||||
:style="{ color: '#FFA031' }"></a-icon>
|
||||
<span>{{ i18n "pages.xray.ipv4RoutingDesc" }}</span>
|
||||
</template>
|
||||
</a-alert>
|
||||
|
|
@ -221,18 +274,22 @@
|
|||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.xray.ipv4Routing" }}</template>
|
||||
<template #control>
|
||||
<a-select mode="tags" :style="{ width: '100%' }" v-model="ipv4Domains"
|
||||
<a-select mode="tags" :style="{ width: '100%' }"
|
||||
v-model="ipv4Domains"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.ServicesOptions">
|
||||
<a-select-option :value="p.value" :label="p.label"
|
||||
v-for="p in settingsData.ServicesOptions">
|
||||
<span>[[ p.label ]]</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
<a-alert type="warning" :style="{ textAlign: 'center', marginTop: '20px' }">
|
||||
<a-alert type="warning"
|
||||
:style="{ textAlign: 'center', marginTop: '20px' }">
|
||||
<template slot="message">
|
||||
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
|
||||
<a-icon type="exclamation-circle" theme="filled"
|
||||
:style="{ color: '#FFA031' }"></a-icon>
|
||||
{{ i18n "pages.xray.warpRoutingDesc" }}
|
||||
</template>
|
||||
</a-alert>
|
||||
|
|
@ -241,20 +298,24 @@
|
|||
<template #title>{{ i18n "pages.xray.warpRouting" }}</template>
|
||||
<template #control>
|
||||
<template v-if="WarpExist">
|
||||
<a-select mode="tags" :style="{ width: '100%' }" v-model="warpDomains"
|
||||
<a-select mode="tags" :style="{ width: '100%' }"
|
||||
v-model="warpDomains"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.ServicesOptions">
|
||||
<a-select-option :value="p.value" :label="p.label"
|
||||
v-for="p in settingsData.ServicesOptions">
|
||||
<span>[[ p.label ]]</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-button type="primary" icon="cloud" @click="showWarp()">WARP</a-button>
|
||||
<a-button type="primary" icon="cloud"
|
||||
@click="showWarp()">WARP</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel key="6" header='{{ i18n "pages.settings.resetDefaultConfig"}}'>
|
||||
<a-collapse-panel key="6"
|
||||
header='{{ i18n "pages.settings.resetDefaultConfig"}}'>
|
||||
<a-space direction="horizontal" :style="{ padding: '0 20px' }">
|
||||
<a-button type="danger" @click="resetXrayConfigToDefault">
|
||||
<span>{{ i18n "pages.settings.resetDefaultConfig" }}</span>
|
||||
|
|
|
|||
|
|
@ -3,18 +3,23 @@
|
|||
<a-row>
|
||||
<a-col :xs="12" :sm="12" :lg="12">
|
||||
<a-space direction="horizontal" size="small">
|
||||
<a-button type="primary" icon="plus" @click="addOutbound()">
|
||||
{{ i18n "pages.xray.outbound.addOutbound" }}
|
||||
<a-button type="primary" icon="plus" @click="addOutbound">
|
||||
<span v-if="!isMobile">{{ i18n
|
||||
"pages.xray.outbound.addOutbound" }}</span>
|
||||
</a-button>
|
||||
<a-button type="primary" icon="cloud" @click="showWarp()">WARP</a-button>
|
||||
<a-button type="primary" icon="cloud"
|
||||
@click="showWarp()">WARP</a-button>
|
||||
</a-space>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="12" :lg="12" :style="{ textAlign: 'right' }">
|
||||
<a-button-group>
|
||||
<a-button icon="sync" @click="refreshOutboundTraffic()" :loading="refreshing"></a-button>
|
||||
<a-popconfirm placement="topRight" @confirm="resetOutboundTraffic(-1)"
|
||||
<a-button icon="sync" @click="refreshOutboundTraffic()"
|
||||
:loading="refreshing"></a-button>
|
||||
<a-popconfirm placement="topRight"
|
||||
@confirm="resetOutboundTraffic(-1)"
|
||||
title='{{ i18n "pages.inbounds.resetTrafficContent"}}'
|
||||
:overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}'
|
||||
:overlay-class-name="themeSwitcher.currentTheme"
|
||||
ok-text='{{ i18n "reset"}}'
|
||||
cancel-text='{{ i18n "cancel"}}'>
|
||||
<a-icon slot="icon" type="question-circle-o"
|
||||
:style="{ color: themeSwitcher.isDarkTheme ? '#008771' : '#008771' }"></a-icon>
|
||||
|
|
@ -23,8 +28,10 @@
|
|||
</a-button-group>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-table :columns="outboundColumns" bordered :row-key="r => r.key" :data-source="outboundData"
|
||||
:scroll="isMobile ? {} : { x: 800 }" :pagination="false" :indent-size="0"
|
||||
<a-table :columns="outboundColumns" bordered :row-key="r => r.key"
|
||||
:data-source="outboundData"
|
||||
:scroll="isMobile ? {} : { x: 800 }" :pagination="false"
|
||||
:indent-size="0"
|
||||
:locale='{ filterConfirm: `{{ i18n "confirm" }}`, filterReset: `{{ i18n "reset" }}` }'>
|
||||
<template slot="action" slot-scope="text, outbound, index">
|
||||
<span>[[ index+1 ]]</span>
|
||||
|
|
@ -32,7 +39,8 @@
|
|||
<a-icon @click="e => e.preventDefault()" type="more"
|
||||
:style="{ fontSize: '16px', textDecoration: 'bold' }"></a-icon>
|
||||
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item v-if="index>0" @click="setFirstOutbound(index)">
|
||||
<a-menu-item v-if="index>0"
|
||||
@click="setFirstOutbound(index)">
|
||||
<a-icon type="vertical-align-top"></a-icon>
|
||||
<span>{{ i18n "pages.xray.rules.first"}}</span>
|
||||
</a-menu-item>
|
||||
|
|
@ -56,21 +64,64 @@
|
|||
</a-dropdown>
|
||||
</template>
|
||||
<template slot="address" slot-scope="text, outbound, index">
|
||||
<p :style="{ margin: '0 5px' }" v-for="addr in findOutboundAddress(outbound)">[[ addr ]]</p>
|
||||
<p :style="{ margin: '0 5px' }"
|
||||
v-for="addr in findOutboundAddress(outbound)">[[ addr ]]</p>
|
||||
</template>
|
||||
<template slot="protocol" slot-scope="text, outbound, index">
|
||||
<a-tag :style="{ margin: '0' }" color="purple">[[ outbound.protocol ]]</a-tag>
|
||||
<a-tag :style="{ margin: '0' }" color="purple">[[ outbound.protocol
|
||||
]]</a-tag>
|
||||
<template
|
||||
v-if="[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
|
||||
<a-tag :style="{ margin: '0' }" color="blue">[[ 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' }" v-if="outbound.streamSettings.security=='reality'"
|
||||
<a-tag :style="{ margin: '0' }" color="blue">[[
|
||||
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' }"
|
||||
v-if="outbound.streamSettings.security=='reality'"
|
||||
color="green">reality</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template slot="traffic" slot-scope="text, outbound, index">
|
||||
<a-tag color="green">[[ findOutboundTraffic(outbound) ]]</a-tag>
|
||||
</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-space>
|
||||
{{end}}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
{{ template "page/head_start" .}}
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/codemirror.min.css?{{ .cur_ver }}">
|
||||
<link rel="stylesheet" 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/codemirror.min.css?{{ .cur_ver }}">
|
||||
<link rel="stylesheet"
|
||||
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">
|
||||
{{ template "page/head_end" .}}
|
||||
|
||||
|
|
@ -10,10 +13,13 @@
|
|||
<a-sidebar></a-sidebar>
|
||||
<a-layout id="content-layout">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
|
||||
<a-spin :spinning="loadingStates.spinning" :delay="500"
|
||||
tip='{{ i18n "loading"}}'>
|
||||
<transition name="list" appear>
|
||||
<a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }"
|
||||
message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
|
||||
<a-alert type="error" v-if="showAlert && loadingStates.fetched"
|
||||
:style="{ marginBottom: '10px' }"
|
||||
message='{{ i18n "secAlertTitle" }}' color="red"
|
||||
description='{{ i18n "secAlertSsl" }}' show-icon closable>
|
||||
</a-alert>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
|
|
@ -26,19 +32,25 @@
|
|||
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
|
||||
<a-col>
|
||||
<a-card hoverable>
|
||||
<a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
|
||||
<a-row
|
||||
:style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
|
||||
<a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
|
||||
<a-space direction="horizontal">
|
||||
<a-button type="primary" :disabled="saveBtnDisable" @click="updateXraySetting">
|
||||
<a-button type="primary" :disabled="saveBtnDisable"
|
||||
@click="updateXraySetting">
|
||||
{{ i18n "pages.xray.save" }}
|
||||
</a-button>
|
||||
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartXray">
|
||||
<a-button type="danger" :disabled="!saveBtnDisable"
|
||||
@click="restartXray">
|
||||
{{ i18n "pages.xray.restart" }}
|
||||
</a-button>
|
||||
<a-popover v-if="restartResult" :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<span slot="title">{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
|
||||
<a-popover v-if="restartResult"
|
||||
:overlay-class-name="themeSwitcher.currentTheme">
|
||||
<span slot="title">{{ i18n
|
||||
"pages.index.xrayErrorPopoverTitle" }}</span>
|
||||
<template slot="content">
|
||||
<span :style="{ maxWidth: '400px' }" v-for="line in restartResult.split('\n')">[[ line
|
||||
<span :style="{ maxWidth: '400px' }"
|
||||
v-for="line in restartResult.split('\n')">[[ line
|
||||
]]</span>
|
||||
</template>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
|
|
@ -48,10 +60,13 @@
|
|||
<a-col :xs="24" :sm="14">
|
||||
<template>
|
||||
<div>
|
||||
<a-back-top :target="() => document.getElementById('content-layout')"
|
||||
<a-back-top
|
||||
:target="() => document.getElementById('content-layout')"
|
||||
visibility-height="200"></a-back-top>
|
||||
<a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }"
|
||||
message='{{ i18n "pages.settings.infoDesc" }}' show-icon>
|
||||
<a-alert type="warning"
|
||||
:style="{ float: 'right', width: 'fit-content' }"
|
||||
message='{{ i18n "pages.settings.infoDesc" }}'
|
||||
show-icon>
|
||||
</a-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -60,7 +75,8 @@
|
|||
</a-card>
|
||||
</a-col>
|
||||
<a-col>
|
||||
<a-tabs default-active-key="tpl-basic" @change="(activeKey) => { this.changePage(activeKey); }"
|
||||
<a-tabs default-active-key="tpl-basic"
|
||||
@change="(activeKey) => { this.changePage(activeKey); }"
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<a-tab-pane key="tpl-basic" :style="{ paddingTop: '20px' }">
|
||||
<template #tab>
|
||||
|
|
@ -83,21 +99,24 @@
|
|||
</template>
|
||||
{{ template "settings/xray/outbounds" . }}
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="tpl-reverse" :style="{ paddingTop: '20px' }" force-render="true">
|
||||
<a-tab-pane key="tpl-reverse" :style="{ paddingTop: '20px' }"
|
||||
force-render="true">
|
||||
<template #tab>
|
||||
<a-icon type="import"></a-icon>
|
||||
<span>{{ i18n "pages.xray.outbound.reverse"}}</span>
|
||||
</template>
|
||||
{{ template "settings/xray/reverse" . }}
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="tpl-balancer" :style="{ paddingTop: '20px' }" force-render="true">
|
||||
<a-tab-pane key="tpl-balancer" :style="{ paddingTop: '20px' }"
|
||||
force-render="true">
|
||||
<template #tab>
|
||||
<a-icon type="cluster"></a-icon>
|
||||
<span>{{ i18n "pages.xray.Balancers"}}</span>
|
||||
</template>
|
||||
{{ template "settings/xray/balancers" . }}
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="tpl-dns" :style="{ paddingTop: '20px' }" force-render="true">
|
||||
<a-tab-pane key="tpl-dns" :style="{ paddingTop: '20px' }"
|
||||
force-render="true">
|
||||
<template #tab>
|
||||
<a-icon type="database"></a-icon>
|
||||
<span>DNS</span>
|
||||
|
|
@ -120,14 +139,18 @@
|
|||
</a-layout>
|
||||
</a-layout>
|
||||
{{template "page/body_scripts" .}}
|
||||
<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/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/javascript.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/lint/lint.js"></script>
|
||||
<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/lint/javascript-lint.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/foldgutter.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/fold/brace-fold.js"></script>
|
||||
|
|
@ -181,11 +204,13 @@
|
|||
];
|
||||
|
||||
const outboundColumns = [
|
||||
{ title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
|
||||
{ title: "#", align: 'center', width: 60, scopedSlots: { customRender: 'action' } },
|
||||
{ title: '{{ i18n "pages.xray.outbound.tag"}}', dataIndex: 'tag', align: 'center', width: 50 },
|
||||
{ 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.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 = [
|
||||
|
|
@ -228,8 +253,11 @@
|
|||
},
|
||||
oldXraySetting: '',
|
||||
xraySetting: '',
|
||||
outboundTestUrl: 'https://www.google.com/generate_204',
|
||||
oldOutboundTestUrl: 'https://www.google.com/generate_204',
|
||||
inboundTags: [],
|
||||
outboundsTraffic: [],
|
||||
outboundTestStates: {}, // Track testing state and results for each outbound
|
||||
saveBtnDisable: true,
|
||||
refreshing: false,
|
||||
restartResult: '',
|
||||
|
|
@ -269,7 +297,7 @@
|
|||
tag: "direct",
|
||||
protocol: "freedom"
|
||||
},
|
||||
routingDomainStrategies: ["AsIs", "IpIfNonMatch", "IpOnDemand"],
|
||||
routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
|
||||
log: {
|
||||
loglevel: ["none", "debug", "info", "warning", "error"],
|
||||
access: ["none", "./access.log"],
|
||||
|
|
@ -337,14 +365,14 @@
|
|||
},
|
||||
defaultObservatory: {
|
||||
subjectSelector: [],
|
||||
probeURL: "http://www.google.com/gen_204",
|
||||
probeURL: "https://www.google.com/generate_204",
|
||||
probeInterval: "10m",
|
||||
enableConcurrency: true
|
||||
},
|
||||
defaultBurstObservatory: {
|
||||
subjectSelector: [],
|
||||
pingConfig: {
|
||||
destination: "http://www.google.com/gen_204",
|
||||
destination: "https://www.google.com/generate_204",
|
||||
interval: "30m",
|
||||
connectivity: "http://connectivitycheck.platform.hicloud.com/generate_204",
|
||||
timeout: "10s",
|
||||
|
|
@ -375,12 +403,17 @@
|
|||
this.oldXraySetting = xs;
|
||||
this.xraySetting = xs;
|
||||
this.inboundTags = result.inboundTags;
|
||||
this.outboundTestUrl = result.outboundTestUrl || 'https://www.google.com/generate_204';
|
||||
this.oldOutboundTestUrl = this.outboundTestUrl;
|
||||
this.saveBtnDisable = true;
|
||||
}
|
||||
},
|
||||
async updateXraySetting() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/panel/xray/update", { xraySetting: this.xraySetting });
|
||||
const msg = await HttpUtil.post("/panel/xray/update", {
|
||||
xraySetting: this.xraySetting,
|
||||
outboundTestUrl: this.outboundTestUrl || 'https://www.google.com/generate_204'
|
||||
});
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
await this.getXraySetting();
|
||||
|
|
@ -595,6 +628,71 @@
|
|||
outbounds.splice(0, 0, outbounds.splice(index, 1)[0]);
|
||||
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() {
|
||||
reverseModal.show({
|
||||
title: '{{ i18n "pages.xray.outbound.addReverse"}}',
|
||||
|
|
@ -968,9 +1066,20 @@
|
|||
await this.getXraySetting();
|
||||
await this.getXrayResult();
|
||||
await this.getOutboundsTraffic();
|
||||
|
||||
if (window.wsClient) {
|
||||
window.wsClient.connect();
|
||||
window.wsClient.on('outbounds', (payload) => {
|
||||
if (payload) {
|
||||
this.outboundsTraffic = payload;
|
||||
this.$forceUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
while (true) {
|
||||
await PromiseUtil.sleep(800);
|
||||
this.saveBtnDisable = this.oldXraySetting === this.xraySetting;
|
||||
this.saveBtnDisable = this.oldXraySetting === this.xraySetting && this.oldOutboundTestUrl === this.outboundTestUrl;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/database"
|
||||
|
|
@ -18,6 +19,12 @@ import (
|
|||
"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.
|
||||
type CheckClientIpJob struct {
|
||||
lastClear int64
|
||||
|
|
@ -119,12 +126,14 @@ func (j *CheckClientIpJob) processLogFile() bool {
|
|||
|
||||
ipRegex := regexp.MustCompile(`from (?:tcp:|udp:)?\[?([0-9a-fA-F\.:]+)\]?:\d+ accepted`)
|
||||
emailRegex := regexp.MustCompile(`email: (.+)$`)
|
||||
timestampRegex := regexp.MustCompile(`^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2})`)
|
||||
|
||||
accessLogPath, _ := xray.GetAccessLogPath()
|
||||
file, _ := os.Open(accessLogPath)
|
||||
defer file.Close()
|
||||
|
||||
inboundClientIps := make(map[string]map[string]struct{}, 100)
|
||||
// Track IPs with their last seen timestamp
|
||||
inboundClientIps := make(map[string]map[string]int64, 100)
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
|
|
@ -147,28 +156,45 @@ func (j *CheckClientIpJob) processLogFile() bool {
|
|||
}
|
||||
email := emailMatches[1]
|
||||
|
||||
if _, exists := inboundClientIps[email]; !exists {
|
||||
inboundClientIps[email] = make(map[string]struct{})
|
||||
// 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 {
|
||||
inboundClientIps[email] = make(map[string]int64)
|
||||
}
|
||||
// Update timestamp - keep the latest
|
||||
if existingTime, ok := inboundClientIps[email][ip]; !ok || timestamp > existingTime {
|
||||
inboundClientIps[email][ip] = timestamp
|
||||
}
|
||||
inboundClientIps[email][ip] = struct{}{}
|
||||
}
|
||||
|
||||
shouldCleanLog := false
|
||||
for email, uniqueIps := range inboundClientIps {
|
||||
for email, ipTimestamps := range inboundClientIps {
|
||||
|
||||
ips := make([]string, 0, len(uniqueIps))
|
||||
for ip := range uniqueIps {
|
||||
ips = append(ips, ip)
|
||||
// Convert to IPWithTimestamp slice
|
||||
ipsWithTime := make([]IPWithTimestamp, 0, len(ipTimestamps))
|
||||
for ip, timestamp := range ipTimestamps {
|
||||
ipsWithTime = append(ipsWithTime, IPWithTimestamp{IP: ip, Timestamp: timestamp})
|
||||
}
|
||||
sort.Strings(ips)
|
||||
|
||||
clientIpsRecord, err := j.getInboundClientIps(email)
|
||||
if err != nil {
|
||||
j.addInboundClientIps(email, ips)
|
||||
j.addInboundClientIps(email, ipsWithTime)
|
||||
continue
|
||||
}
|
||||
|
||||
shouldCleanLog = j.updateInboundClientIps(clientIpsRecord, email, ips) || shouldCleanLog
|
||||
shouldCleanLog = j.updateInboundClientIps(clientIpsRecord, email, ipsWithTime) || shouldCleanLog
|
||||
}
|
||||
|
||||
return shouldCleanLog
|
||||
|
|
@ -213,9 +239,9 @@ func (j *CheckClientIpJob) getInboundClientIps(clientEmail string) (*model.Inbou
|
|||
return InboundClientIps, nil
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) addInboundClientIps(clientEmail string, ips []string) error {
|
||||
func (j *CheckClientIpJob) addInboundClientIps(clientEmail string, ipsWithTime []IPWithTimestamp) error {
|
||||
inboundClientIps := &model.InboundClientIps{}
|
||||
jsonIps, err := json.Marshal(ips)
|
||||
jsonIps, err := json.Marshal(ipsWithTime)
|
||||
j.checkError(err)
|
||||
|
||||
inboundClientIps.ClientEmail = clientEmail
|
||||
|
|
@ -239,16 +265,8 @@ func (j *CheckClientIpJob) addInboundClientIps(clientEmail string, ips []string)
|
|||
return nil
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, ips []string) bool {
|
||||
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)
|
||||
|
||||
func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, newIpsWithTime []IPWithTimestamp) bool {
|
||||
// Get the inbound configuration
|
||||
inbound, err := j.getInboundByEmail(clientEmail)
|
||||
if err != nil {
|
||||
logger.Errorf("failed to fetch inbound settings for email %s: %s", clientEmail, err)
|
||||
|
|
@ -263,9 +281,57 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
|
|||
settings := map[string][]model.Client{}
|
||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
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
|
||||
j.disAllowedIps = []string{}
|
||||
|
||||
// Open log file
|
||||
logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
logger.Errorf("failed to open IP limit log file: %s", err)
|
||||
|
|
@ -275,27 +341,33 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
|
|||
log.SetOutput(logIpFile)
|
||||
log.SetFlags(log.LstdFlags)
|
||||
|
||||
for _, client := range clients {
|
||||
if client.Email == clientEmail {
|
||||
limitIp := client.LimitIP
|
||||
// Check if we exceed the limit
|
||||
if len(allIps) > limitIp {
|
||||
shouldCleanLog = true
|
||||
|
||||
if limitIp > 0 && inbound.Enable {
|
||||
shouldCleanLog = true
|
||||
// Keep only the newest IPs (up to limitIp)
|
||||
keptIps := allIps[:limitIp]
|
||||
disconnectedIps := allIps[limitIp:]
|
||||
|
||||
if limitIp < len(ips) {
|
||||
j.disAllowedIps = append(j.disAllowedIps, ips[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)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(j.disAllowedIps)
|
||||
// Actually disconnect old IPs by temporarily removing and re-adding user
|
||||
// This forces Xray to drop existing connections from old IPs
|
||||
if len(disconnectedIps) > 0 {
|
||||
j.disconnectClientTemporarily(inbound, clientEmail, clients)
|
||||
}
|
||||
|
||||
if len(j.disAllowedIps) > 0 {
|
||||
logger.Debug("disAllowedIps:", j.disAllowedIps)
|
||||
// Update database with only the newest IPs
|
||||
jsonIps, _ := json.Marshal(keptIps)
|
||||
inboundClientIps.Ips = string(jsonIps)
|
||||
} else {
|
||||
// Under limit, save all IPs
|
||||
jsonIps, _ := json.Marshal(allIps)
|
||||
inboundClientIps.Ips = string(jsonIps)
|
||||
}
|
||||
|
||||
db := database.GetDB()
|
||||
|
|
@ -305,9 +377,68 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
|
|||
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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
db := database.GetDB()
|
||||
inbound := &model.Inbound{}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ func (j *ClearLogsJob) Run() {
|
|||
}
|
||||
|
||||
// Clear log files and copy to previous logs
|
||||
for i := 0; i < len(logFiles); i++ {
|
||||
for i := range len(logFiles) {
|
||||
if i > 0 {
|
||||
// Copy to previous logs
|
||||
logFilePrev, err := os.OpenFile(logFilesPrev[i-1], os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
|
||||
|
|
|
|||
|
|
@ -58,14 +58,36 @@ func (j *XrayTrafficJob) Run() {
|
|||
lastOnlineMap = make(map[string]int64)
|
||||
}
|
||||
|
||||
// Broadcast traffic update via WebSocket
|
||||
trafficUpdate := map[string]interface{}{
|
||||
// Fetch updated inbounds from database with accumulated traffic values
|
||||
// This ensures frontend receives the actual total traffic, not just delta values
|
||||
updatedInbounds, err := j.inboundService.GetAllInbounds()
|
||||
if err != nil {
|
||||
logger.Warning("get all inbounds for websocket failed:", err)
|
||||
}
|
||||
|
||||
updatedOutbounds, err := j.outboundService.GetOutboundsTraffic()
|
||||
if err != nil {
|
||||
logger.Warning("get all outbounds for websocket failed:", err)
|
||||
}
|
||||
|
||||
// Broadcast traffic update via WebSocket with accumulated values from database
|
||||
trafficUpdate := map[string]any{
|
||||
"traffics": traffics,
|
||||
"clientTraffics": clientTraffics,
|
||||
"onlineClients": onlineClients,
|
||||
"lastOnlineMap": lastOnlineMap,
|
||||
}
|
||||
websocket.BroadcastTraffic(trafficUpdate)
|
||||
|
||||
// Broadcast full inbounds update for real-time UI refresh
|
||||
if updatedInbounds != nil {
|
||||
websocket.BroadcastInbounds(updatedInbounds)
|
||||
}
|
||||
|
||||
if updatedOutbounds != nil {
|
||||
websocket.BroadcastOutbounds(updatedOutbounds)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (j *XrayTrafficJob) informTrafficToExternalAPI(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) {
|
||||
|
|
|
|||
|
|
@ -1010,12 +1010,12 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr
|
|||
if len(traffics) == 0 {
|
||||
// Empty onlineUsers
|
||||
if p != nil {
|
||||
p.SetOnlineClients(nil)
|
||||
p.SetOnlineClients(make([]string, 0))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var onlineClients []string
|
||||
onlineClients := make([]string, 0)
|
||||
|
||||
emails := make([]string, 0, len(traffics))
|
||||
for _, traffic := range traffics {
|
||||
|
|
@ -2141,6 +2141,43 @@ func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error)
|
|||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,22 @@
|
|||
package service
|
||||
|
||||
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/model"
|
||||
"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"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
|
@ -13,6 +26,9 @@ import (
|
|||
// It handles outbound traffic monitoring and statistics.
|
||||
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) {
|
||||
var err error
|
||||
db := database.GetDB()
|
||||
|
|
@ -100,3 +116,307 @@ func (s *OutboundService) ResetOutboundTraffic(tag string) error {
|
|||
|
||||
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
|
||||
}
|
||||
|
||||
if major > 25 || (major == 25 && minor > 9) || (major == 25 && minor == 9 && patch >= 11) {
|
||||
if major > 26 || (major == 26 && minor > 2) || (major == 26 && minor == 2 && patch >= 6) {
|
||||
versions = append(versions, release.TagName)
|
||||
}
|
||||
}
|
||||
|
|
@ -1056,44 +1056,79 @@ func (s *ServerService) IsValidGeofileName(filename string) bool {
|
|||
}
|
||||
|
||||
func (s *ServerService) UpdateGeofile(fileName string) error {
|
||||
files := []struct {
|
||||
type geofileEntry struct {
|
||||
URL string
|
||||
FileName string
|
||||
}{
|
||||
{"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"},
|
||||
{"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"},
|
||||
{"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"},
|
||||
}
|
||||
geofileAllowlist := map[string]geofileEntry{
|
||||
"geoip.dat": {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip.dat"},
|
||||
"geosite.dat": {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite.dat"},
|
||||
"geoip_IR.dat": {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat", "geoip_IR.dat"},
|
||||
"geosite_IR.dat": {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat", "geosite_IR.dat"},
|
||||
"geoip_RU.dat": {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip_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
|
||||
if fileName != "" {
|
||||
// Use the centralized validation function
|
||||
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)
|
||||
if _, ok := geofileAllowlist[fileName]; !ok {
|
||||
return common.NewErrorf("Invalid geofile name: %q not in allowlist", fileName)
|
||||
}
|
||||
}
|
||||
|
||||
downloadFile := func(url, destPath string) error {
|
||||
resp, err := http.Get(url)
|
||||
var req *http.Request
|
||||
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 {
|
||||
return common.NewErrorf("Failed to download Geofile from %s: %v", url, err)
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return common.NewErrorf("Failed to create Geofile %s: %v", destPath, err)
|
||||
|
|
@ -1105,39 +1140,25 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
|
|||
return common.NewErrorf("Failed to save Geofile %s: %v", destPath, err)
|
||||
}
|
||||
|
||||
updateFileModTime()
|
||||
return nil
|
||||
}
|
||||
|
||||
var errorMessages []string
|
||||
|
||||
if fileName == "" {
|
||||
for _, file := range files {
|
||||
// Sanitize the filename from our allowlist as an extra precaution
|
||||
destPath := filepath.Join(config.GetBinFolderPath(), filepath.Base(file.FileName))
|
||||
|
||||
if err := downloadFile(file.URL, destPath); err != nil {
|
||||
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", file.FileName, err))
|
||||
// Download all geofiles
|
||||
for _, entry := range geofileAllowlist {
|
||||
destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName)
|
||||
if err := downloadFile(entry.URL, destPath); err != nil {
|
||||
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use filepath.Base to ensure we only get the filename component, no path traversal
|
||||
safeName := filepath.Base(fileName)
|
||||
destPath := filepath.Join(config.GetBinFolderPath(), safeName)
|
||||
|
||||
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))
|
||||
}
|
||||
entry := geofileAllowlist[fileName]
|
||||
destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName)
|
||||
if err := downloadFile(entry.URL, destPath); err != nil {
|
||||
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1205,7 +1226,7 @@ func (s *ServerService) GetNewmldsa65() (any, error) {
|
|||
return keyPair, nil
|
||||
}
|
||||
|
||||
func (s *ServerService) GetNewEchCert(sni string) (interface{}, error) {
|
||||
func (s *ServerService) GetNewEchCert(sni string) (any, error) {
|
||||
// Run the command
|
||||
cmd := exec.Command(xray.GetBinaryPath(), "tls", "ech", "--serverName", sni)
|
||||
var out bytes.Buffer
|
||||
|
|
@ -1223,7 +1244,7 @@ func (s *ServerService) GetNewEchCert(sni string) (interface{}, error) {
|
|||
configList := lines[1]
|
||||
serverKeys := lines[3]
|
||||
|
||||
return map[string]interface{}{
|
||||
return map[string]any{
|
||||
"echServerKeys": serverKeys,
|
||||
"echConfigList": configList,
|
||||
}, nil
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -53,6 +54,11 @@ var defaultValueMap = map[string]string{
|
|||
"subEnable": "true",
|
||||
"subJsonEnable": "false",
|
||||
"subTitle": "",
|
||||
"subSupportUrl": "",
|
||||
"subProfileUrl": "",
|
||||
"subAnnounce": "",
|
||||
"subEnableRouting": "true",
|
||||
"subRoutingRules": "",
|
||||
"subListen": "",
|
||||
"subPort": "2096",
|
||||
"subPath": "/sub/",
|
||||
|
|
@ -73,6 +79,8 @@ var defaultValueMap = map[string]string{
|
|||
"warp": "",
|
||||
"externalTrafficInformEnable": "false",
|
||||
"externalTrafficInformURI": "",
|
||||
"xrayOutboundTestUrl": "https://www.google.com/generate_204",
|
||||
|
||||
// LDAP defaults
|
||||
"ldapEnable": "false",
|
||||
"ldapHost": "",
|
||||
|
|
@ -266,6 +274,14 @@ func (s *SettingService) GetXrayConfigTemplate() (string, error) {
|
|||
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) {
|
||||
return s.getString("webListen")
|
||||
}
|
||||
|
|
@ -459,6 +475,26 @@ func (s *SettingService) GetSubTitle() (string, error) {
|
|||
return s.getString("subTitle")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubSupportUrl() (string, error) {
|
||||
return s.getString("subSupportUrl")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubProfileUrl() (string, error) {
|
||||
return s.getString("subProfileUrl")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubAnnounce() (string, error) {
|
||||
return s.getString("subAnnounce")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubEnableRouting() (bool, error) {
|
||||
return s.getBool("subEnableRouting")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubRoutingRules() (string, error) {
|
||||
return s.getString("subRoutingRules")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubListen() (string, error) {
|
||||
return s.getString("subListen")
|
||||
}
|
||||
|
|
@ -682,6 +718,28 @@ func (s *SettingService) GetDefaultXrayConfig() (any, error) {
|
|||
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) {
|
||||
type settingFunc func() (any, error)
|
||||
settings := map[string]settingFunc{
|
||||
|
|
@ -732,7 +790,7 @@ func (s *SettingService) GetDefaultSettings(host string) (any, error) {
|
|||
subTLS = true
|
||||
}
|
||||
if subDomain == "" {
|
||||
subDomain = strings.Split(host, ":")[0]
|
||||
subDomain = extractHostname(host)
|
||||
}
|
||||
if subTLS {
|
||||
subURI = "https://"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"crypto/rand"
|
||||
"embed"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -2267,6 +2268,8 @@ func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {
|
|||
}
|
||||
|
||||
// Gather settings to construct absolute URLs
|
||||
subURI, _ := t.settingService.GetSubURI()
|
||||
subJsonURI, _ := t.settingService.GetSubJsonURI()
|
||||
subDomain, _ := t.settingService.GetSubDomain()
|
||||
subPort, _ := t.settingService.GetSubPort()
|
||||
subPath, _ := t.settingService.GetSubPath()
|
||||
|
|
@ -2314,8 +2317,29 @@ func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {
|
|||
subJsonPath = subJsonPath + "/"
|
||||
}
|
||||
|
||||
subURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subPath, client.SubID)
|
||||
subJsonURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subJsonPath, client.SubID)
|
||||
var subURL string
|
||||
var subJsonURL string
|
||||
|
||||
// If pre-configured URIs are available, use them directly
|
||||
if subURI != "" {
|
||||
if !strings.HasSuffix(subURI, "/") {
|
||||
subURI = subURI + "/"
|
||||
}
|
||||
subURL = fmt.Sprintf("%s%s", subURI, client.SubID)
|
||||
} else {
|
||||
subURL = fmt.Sprintf("%s://%s%s%s", scheme, host, subPath, client.SubID)
|
||||
}
|
||||
|
||||
if subJsonURI != "" {
|
||||
if !strings.HasSuffix(subJsonURI, "/") {
|
||||
subJsonURI = subJsonURI + "/"
|
||||
}
|
||||
subJsonURL = fmt.Sprintf("%s%s", subJsonURI, client.SubID)
|
||||
} else {
|
||||
|
||||
subJsonURL = fmt.Sprintf("%s://%s%s%s", scheme, host, subJsonPath, client.SubID)
|
||||
}
|
||||
|
||||
if !subJsonEnable {
|
||||
subJsonURL = ""
|
||||
}
|
||||
|
|
@ -3060,9 +3084,41 @@ func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
|
|||
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 += t.I18nBot("tgbot.messages.email", "Email=="+email)
|
||||
output += t.I18nBot("tgbot.messages.ips", "IPs=="+ips)
|
||||
output += t.I18nBot("tgbot.messages.ips", "IPs=="+formattedIps)
|
||||
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
inlineKeyboard := tu.InlineKeyboard(
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@ func (s *XrayService) GetXrayErr() error {
|
|||
}
|
||||
|
||||
err := p.GetErr()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" && err.Error() == "exit status 1" {
|
||||
// exit status 1 on Windows means that Xray process was killed
|
||||
|
|
|
|||
|
|
@ -374,6 +374,16 @@
|
|||
"subJsonEnable" = "تمكين/تعطيل نقطة نهاية اشتراك JSON بشكل مستقل."
|
||||
"subTitle" = "عنوان الاشتراك"
|
||||
"subTitleDesc" = "العنوان اللي هيظهر في عميل VPN"
|
||||
"subSupportUrl" = "رابط الدعم"
|
||||
"subSupportUrlDesc" = "رابط الدعم الفني المعروض في عميل VPN"
|
||||
"subProfileUrl" = "رابط الملف الشخصي"
|
||||
"subProfileUrlDesc" = "رابط لموقعك الإلكتروني يظهر في عميل VPN"
|
||||
"subAnnounce" = "إعلان"
|
||||
"subAnnounceDesc" = "نص الإعلان المعروض في عميل VPN"
|
||||
"subEnableRouting" = "تفعيل التوجيه"
|
||||
"subEnableRoutingDesc" = "إعداد عام لتمكين التوجيه (Routing) في عميل VPN. (فقط لـ Happ)"
|
||||
"subRoutingRules" = "قواعد التوجيه"
|
||||
"subRoutingRulesDesc" = "قواعد التوجيه العامة لعميل VPN. (فقط لـ Happ)"
|
||||
"subListen" = "IP الاستماع"
|
||||
"subListenDesc" = "عنوان IP لخدمة الاشتراك. (سيبه فاضي عشان يستمع على كل الـ IPs)"
|
||||
"subPort" = "بورت الاستماع"
|
||||
|
|
@ -450,6 +460,8 @@
|
|||
"FreedomStrategyDesc" = "اختار استراتيجية المخرجات للشبكة في بروتوكول الحرية."
|
||||
"RoutingStrategy" = "استراتيجية التوجيه العامة"
|
||||
"RoutingStrategyDesc" = "حدد استراتيجية التوجيه الإجمالية لحل كل الطلبات."
|
||||
"outboundTestUrl" = "رابط اختبار المخرج"
|
||||
"outboundTestUrlDesc" = "الرابط المستخدم عند اختبار اتصال المخرج"
|
||||
"Torrent" = "حظر بروتوكول التورنت"
|
||||
"Inbounds" = "الإدخالات"
|
||||
"InboundsDesc" = "قبول العملاء المعينين."
|
||||
|
|
@ -531,6 +543,12 @@
|
|||
"psk" = "المفتاح المشترك"
|
||||
"domainStrategy" = "استراتيجية الدومين"
|
||||
|
||||
[pages.xray.tun]
|
||||
"nameDesc" = "اسم واجهة TUN. القيمة الافتراضية هي 'xray0'"
|
||||
"mtuDesc" = "وحدة النقل الأقصى. الحد الأقصى لحجم حزم البيانات. القيمة الافتراضية هي 1500"
|
||||
"userLevel" = "مستوى المستخدم"
|
||||
"userLevelDesc" = "ستستخدم جميع الاتصالات المُرسلة عبر هذا الإدخال مستوى المستخدم هذا. القيمة الافتراضية هي 0"
|
||||
|
||||
[pages.xray.dns]
|
||||
"enable" = "فعل DNS"
|
||||
"enableDesc" = "فعل سيرفر DNS المدمج"
|
||||
|
|
|
|||
|
|
@ -374,6 +374,16 @@
|
|||
"subJsonEnable" = "Enable/Disable the JSON subscription endpoint independently."
|
||||
"subTitle" = "Subscription Title"
|
||||
"subTitleDesc" = "Title shown in VPN client"
|
||||
"subSupportUrl" = "Support URL"
|
||||
"subSupportUrlDesc" = "Technical support link shown in the VPN client"
|
||||
"subProfileUrl" = "Profile URL"
|
||||
"subProfileUrlDesc" = "A link to your website displayed in the VPN client"
|
||||
"subAnnounce" = "Announce"
|
||||
"subAnnounceDesc" = "The text of the announce displayed in the VPN client"
|
||||
"subEnableRouting" = "Enable routing"
|
||||
"subEnableRoutingDesc" = "Global setting to enable routing in the VPN client. (Only for Happ)"
|
||||
"subRoutingRules" = "Routing rules"
|
||||
"subRoutingRulesDesc" = "Global routing rules for the VPN client. (Only for Happ)"
|
||||
"subListen" = "Listen IP"
|
||||
"subListenDesc" = "The IP address for the subscription service. (leave blank to listen on all IPs)"
|
||||
"subPort" = "Listen Port"
|
||||
|
|
@ -450,6 +460,8 @@
|
|||
"FreedomStrategyDesc" = "Set the output strategy for the network in the Freedom Protocol."
|
||||
"RoutingStrategy" = "Overall Routing Strategy"
|
||||
"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"
|
||||
"Inbounds" = "Inbounds"
|
||||
"InboundsDesc" = "Accepting the specific clients."
|
||||
|
|
@ -513,6 +525,12 @@
|
|||
"accountInfo" = "Account Information"
|
||||
"outboundStatus" = "Outbound Status"
|
||||
"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]
|
||||
"addBalancer" = "Add Balancer"
|
||||
|
|
@ -531,6 +549,12 @@
|
|||
"psk" = "PreShared Key"
|
||||
"domainStrategy" = "Domain Strategy"
|
||||
|
||||
[pages.xray.tun]
|
||||
"nameDesc" = "The name of the TUN interface. Default is 'xray0'"
|
||||
"mtuDesc" = "Maximum Transmission Unit. The maximum size of data packets. Default is 1500"
|
||||
"userLevel" = "User Level"
|
||||
"userLevelDesc" = "All connections made through this inbound will use this user level. Default is 0"
|
||||
|
||||
[pages.xray.dns]
|
||||
"enable" = "Enable DNS"
|
||||
"enableDesc" = "Enable built-in DNS server"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
"copy" = "Copiar"
|
||||
"copied" = "Copiado"
|
||||
"download" = "Descargar"
|
||||
"remark" = "Nota"
|
||||
"remark" = "Notas"
|
||||
"enable" = "Habilitar"
|
||||
"protocol" = "Protocolo"
|
||||
"search" = "Buscar"
|
||||
|
|
@ -28,14 +28,14 @@
|
|||
"edit" = "Editar"
|
||||
"delete" = "Eliminar"
|
||||
"reset" = "Restablecer"
|
||||
"noData" = "Sin datos."
|
||||
"noData" = "Sin datos"
|
||||
"copySuccess" = "Copiado exitosamente"
|
||||
"sure" = "Seguro"
|
||||
"encryption" = "Encriptación"
|
||||
"useIPv4ForHost" = "Usar IPv4 para el host"
|
||||
"transmission" = "Transmisión"
|
||||
"host" = "Anfitrión"
|
||||
"path" = "Ruta"
|
||||
"host" = "Host"
|
||||
"path" = "Path"
|
||||
"camouflage" = "Camuflaje"
|
||||
"status" = "Estado"
|
||||
"enabled" = "Habilitado"
|
||||
|
|
@ -114,7 +114,7 @@
|
|||
"cpu" = "CPU"
|
||||
"logicalProcessors" = "Procesadores lógicos"
|
||||
"frequency" = "Frecuencia"
|
||||
"swap" = "Intercambio"
|
||||
"swap" = "Memoria Virtual"
|
||||
"storage" = "Almacenamiento"
|
||||
"memory" = "RAM"
|
||||
"threads" = "Hilos"
|
||||
|
|
@ -167,7 +167,7 @@
|
|||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "Tráfico Total"
|
||||
"allTimeTrafficUsage" = "Uso total de todos los tiempos"
|
||||
"allTimeTrafficUsage" = "Uso de datos histórico"
|
||||
"title" = "Entradas"
|
||||
"totalDownUp" = "Subidas/Descargas Totales"
|
||||
"totalUsage" = "Uso Total"
|
||||
|
|
@ -201,7 +201,7 @@
|
|||
"destinationPort" = "Puerto de Destino"
|
||||
"targetAddress" = "Dirección de Destino"
|
||||
"monitorDesc" = "Dejar en blanco por defecto"
|
||||
"meansNoLimit" = "= illimitata. (unidad: GB)"
|
||||
"meansNoLimit" = " = illimitata. (unidad: GB)"
|
||||
"totalFlow" = "Flujo Total"
|
||||
"leaveBlankToNeverExpire" = "Dejar en Blanco para Nunca Expirar"
|
||||
"noRecommendKeepDefault" = "No hay requisitos especiales para mantener la configuración predeterminada"
|
||||
|
|
@ -283,7 +283,7 @@
|
|||
"inboundClientAddSuccess" = "Cliente(s) de entrada añadido(s)"
|
||||
"inboundClientDeleteSuccess" = "Cliente de entrada eliminado"
|
||||
"inboundClientUpdateSuccess" = "Cliente de entrada actualizado"
|
||||
"delDepletedClientsSuccess" = "Todos los clientes agotados fueron eliminados"
|
||||
"delDepletedClientsSuccess" = "Todos los clientes con tráfico agotado fueron eliminados"
|
||||
"resetAllClientTrafficSuccess" = "Todo el tráfico del cliente ha sido reiniciado"
|
||||
"resetAllTrafficSuccess" = "Todo el tráfico ha sido reiniciado"
|
||||
"resetInboundClientTrafficSuccess" = "El tráfico ha sido reiniciado"
|
||||
|
|
@ -373,7 +373,17 @@
|
|||
"subEnableDesc" = "Función de suscripción con configuración separada."
|
||||
"subJsonEnable" = "Habilitar/Deshabilitar el endpoint de suscripción JSON de forma independiente."
|
||||
"subTitle" = "Título de la Suscripción"
|
||||
"subTitleDesc" = "Título mostrado en el cliente de VPN"
|
||||
"subTitleDesc" = "Título mostrado en el cliente VPN"
|
||||
"subSupportUrl" = "URL de soporte"
|
||||
"subSupportUrlDesc" = "Enlace de soporte técnico mostrado en el cliente VPN"
|
||||
"subProfileUrl" = "URL del perfil"
|
||||
"subProfileUrlDesc" = "Un enlace a tu sitio web mostrado en el cliente VPN"
|
||||
"subAnnounce" = "Anuncio"
|
||||
"subAnnounceDesc" = "El texto del anuncio mostrado en el cliente VPN"
|
||||
"subEnableRouting" = "Habilitar enrutamiento"
|
||||
"subEnableRoutingDesc" = "Configuración global para habilitar el enrutamiento en el cliente VPN. (Solo para Happ)"
|
||||
"subRoutingRules" = "Reglas de enrutamiento"
|
||||
"subRoutingRulesDesc" = "Reglas de enrutamiento globales para el cliente VPN. (Solo para Happ)"
|
||||
"subListen" = "Listening IP"
|
||||
"subListenDesc" = "Dejar en blanco por defecto para monitorear todas las IPs."
|
||||
"subPort" = "Puerto de Suscripción"
|
||||
|
|
@ -401,8 +411,8 @@
|
|||
"fragment" = "Fragmentación"
|
||||
"fragmentDesc" = "Habilitar la fragmentación para el paquete de saludo de TLS"
|
||||
"fragmentSett" = "Configuración de Fragmentación"
|
||||
"noisesDesc" = "Activar Noises."
|
||||
"noisesSett" = "Configuración de Noises"
|
||||
"noisesDesc" = "Activar Sonidos"
|
||||
"noisesSett" = "Configuración de Sonidos"
|
||||
"mux" = "Mux"
|
||||
"muxDesc" = "Transmite múltiples flujos de datos independientes dentro de un flujo de datos establecido."
|
||||
"muxSett" = "Configuración Mux"
|
||||
|
|
@ -426,8 +436,8 @@
|
|||
"stopSuccess" = "Xray se ha detenido correctamente"
|
||||
"restartError" = "Ocurrió un error al reiniciar Xray."
|
||||
"stopError" = "Ocurrió un error al detener Xray."
|
||||
"basicTemplate" = "Plantilla Básica"
|
||||
"advancedTemplate" = "Plantilla Avanzada"
|
||||
"basicTemplate" = "Perfil Básico"
|
||||
"advancedTemplate" = "Perfil Avanzado"
|
||||
"generalConfigs" = "Configuraciones Generales"
|
||||
"generalConfigsDesc" = "Estas opciones proporcionarán ajustes generales."
|
||||
"logConfigs" = "Registro"
|
||||
|
|
@ -450,6 +460,8 @@
|
|||
"FreedomStrategyDesc" = "Establece la estrategia de salida de la red en el Protocolo Freedom."
|
||||
"RoutingStrategy" = "Configurar Estrategia de Enrutamiento de Dominios"
|
||||
"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"
|
||||
"Inbounds" = "Entrante"
|
||||
"InboundsDesc" = "Cambia la plantilla de configuración para aceptar clientes específicos."
|
||||
|
|
@ -531,6 +543,12 @@
|
|||
"psk" = "Clave precompartida"
|
||||
"domainStrategy" = "Estrategia de dominio"
|
||||
|
||||
[pages.xray.tun]
|
||||
"nameDesc" = "El nombre de la interfaz TUN. El valor predeterminado es 'xray0'"
|
||||
"mtuDesc" = "Unidad Máxima de Transmisión. El tamaño máximo de los paquetes de datos. El valor predeterminado es 1500"
|
||||
"userLevel" = "Nivel de Usuario"
|
||||
"userLevelDesc" = "Todas las conexiones realizadas a través de este entrada utilizarán este nivel de usuario. El valor predeterminado es 0"
|
||||
|
||||
[pages.xray.dns]
|
||||
"enable" = "Habilitar DNS"
|
||||
"enableDesc" = "Habilitar servidor DNS incorporado"
|
||||
|
|
@ -594,8 +612,8 @@
|
|||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ Teclado cerrado!"
|
||||
"noResult" = "❗ ¡No hay resultados!"
|
||||
"noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor, use el comando de nuevo!"
|
||||
"noResult" = "❗ ¡Sin resultados!"
|
||||
"noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor, use el comando nuevamente!"
|
||||
"wentWrong" = "❌ ¡Algo salió mal!"
|
||||
"noIpRecord" = "❗ ¡No hay registro de IP!"
|
||||
"noInbounds" = "❗ ¡No se encontraron entradas!"
|
||||
|
|
|
|||
|
|
@ -374,6 +374,16 @@
|
|||
"subJsonEnable" = "فعال/غیرفعالسازی مستقل نقطه دسترسی سابسکریپشن JSON."
|
||||
"subTitle" = "عنوان اشتراک"
|
||||
"subTitleDesc" = "عنوان نمایش داده شده در کلاینت VPN"
|
||||
"subSupportUrl" = "آدرس پشتیبانی"
|
||||
"subSupportUrlDesc" = "لینک پشتیبانی فنی که در کلاینت VPN نمایش داده میشود"
|
||||
"subProfileUrl" = "آدرس پروفایل"
|
||||
"subProfileUrlDesc" = "لینک وبسایت شما که در کلاینت VPN نمایش داده میشود"
|
||||
"subAnnounce" = "اعلان"
|
||||
"subAnnounceDesc" = "متن اعلانی که در کلاینت VPN نمایش داده میشود"
|
||||
"subEnableRouting" = "فعالسازی مسیریابی"
|
||||
"subEnableRoutingDesc" = "تنظیمات سراسری برای فعالسازی مسیریابی در کلاینت VPN. (فقط برای Happ)"
|
||||
"subRoutingRules" = "قوانین مسیریابی"
|
||||
"subRoutingRulesDesc" = "قوانین مسیریابی سراسری برای کلاینت VPN. (فقط برای Happ)"
|
||||
"subListen" = "آدرس آیپی"
|
||||
"subListenDesc" = "آدرس آیپی برای سرویس سابسکریپشن. برای گوش دادن بهتمام آیپیها خالیبگذارید"
|
||||
"subPort" = "پورت"
|
||||
|
|
@ -450,6 +460,8 @@
|
|||
"FreedomStrategyDesc" = "تعیین میکند Freedom استراتژی خروجی شبکه را برای پروتکل"
|
||||
"RoutingStrategy" = "استراتژی کلی مسیریابی"
|
||||
"RoutingStrategyDesc" = "استراتژی کلی مسیریابی برای حل تمام درخواستها را تعیین میکند"
|
||||
"outboundTestUrl" = "آدرس تست خروجی"
|
||||
"outboundTestUrlDesc" = "آدرسی که برای تست اتصال خروجی استفاده میشود."
|
||||
"Torrent" = "مسدودسازی پروتکل بیتتورنت"
|
||||
"Inbounds" = "ورودیها"
|
||||
"InboundsDesc" = "پذیرش کلاینت خاص"
|
||||
|
|
@ -531,6 +543,12 @@
|
|||
"psk" = "کلید مشترک"
|
||||
"domainStrategy" = "استراتژی حل دامنه"
|
||||
|
||||
[pages.xray.tun]
|
||||
"nameDesc" = "نام رابط TUN. مقدار پیشفرض 'xray0' است"
|
||||
"mtuDesc" = "واحد انتقال حداکثر. بیشترین اندازه بستههای داده. مقدار پیشفرض 1500 است"
|
||||
"userLevel" = "سطح کاربر"
|
||||
"userLevelDesc" = "تمام اتصالات انجامشده از طریق این ورودی از این سطح کاربری استفاده خواهند کرد. مقدار پیشفرض 0 است"
|
||||
|
||||
[pages.xray.dns]
|
||||
"enable" = "فعال کردن حل دامنه"
|
||||
"enableDesc" = "سرور حل دامنه داخلی را فعال کنید"
|
||||
|
|
|
|||
|
|
@ -374,6 +374,16 @@
|
|||
"subJsonEnable" = "Aktifkan/Nonaktifkan endpoint langganan JSON secara mandiri."
|
||||
"subTitle" = "Judul Langganan"
|
||||
"subTitleDesc" = "Judul yang ditampilkan di klien VPN"
|
||||
"subSupportUrl" = "URL Dukungan"
|
||||
"subSupportUrlDesc" = "Tautan dukungan teknis yang ditampilkan di klien VPN"
|
||||
"subProfileUrl" = "URL Profil"
|
||||
"subProfileUrlDesc" = "Tautan ke situs web Anda yang ditampilkan di klien VPN"
|
||||
"subAnnounce" = "Pengumuman"
|
||||
"subAnnounceDesc" = "Teks pengumuman yang ditampilkan di klien VPN"
|
||||
"subEnableRouting" = "Aktifkan perutean"
|
||||
"subEnableRoutingDesc" = "Pengaturan global untuk mengaktifkan perutean (routing) di klien VPN. (Hanya untuk Happ)"
|
||||
"subRoutingRules" = "Aturan routing"
|
||||
"subRoutingRulesDesc" = "Aturan routing global untuk klien VPN. (Hanya untuk Happ)"
|
||||
"subListen" = "IP Pendengar"
|
||||
"subListenDesc" = "Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)"
|
||||
"subPort" = "Port Pendengar"
|
||||
|
|
@ -450,6 +460,8 @@
|
|||
"FreedomStrategyDesc" = "Atur strategi output untuk jaringan dalam Protokol Freedom."
|
||||
"RoutingStrategy" = "Strategi Pengalihan Keseluruhan"
|
||||
"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"
|
||||
"Inbounds" = "Masuk"
|
||||
"InboundsDesc" = "Menerima klien tertentu."
|
||||
|
|
@ -531,6 +543,12 @@
|
|||
"psk" = "Kunci Pra-Bagi"
|
||||
"domainStrategy" = "Strategi Domain"
|
||||
|
||||
[pages.xray.tun]
|
||||
"nameDesc" = "Nama antarmuka TUN. Standar adalah 'xray0'"
|
||||
"mtuDesc" = "Unit Transmisi Maksimum. Ukuran maksimum paket data. Standar adalah 1500"
|
||||
"userLevel" = "Level Pengguna"
|
||||
"userLevelDesc" = "Semua koneksi yang dibuat melalui inbound ini akan menggunakan level pengguna ini. Standar adalah 0"
|
||||
|
||||
[pages.xray.dns]
|
||||
"enable" = "Aktifkan DNS"
|
||||
"enableDesc" = "Aktifkan server DNS bawaan"
|
||||
|
|
|
|||
|
|
@ -374,6 +374,16 @@
|
|||
"subJsonEnable" = "JSON サブスクリプションのエンドポイントを個別に有効/無効にする。"
|
||||
"subTitle" = "サブスクリプションタイトル"
|
||||
"subTitleDesc" = "VPNクライアントに表示されるタイトル"
|
||||
"subSupportUrl" = "サポートURL"
|
||||
"subSupportUrlDesc" = "VPNクライアントに表示されるテクニカルサポートへのリンク"
|
||||
"subProfileUrl" = "プロフィールURL"
|
||||
"subProfileUrlDesc" = "VPNクライアントに表示されるWebサイトへのリンク"
|
||||
"subAnnounce" = "お知らせ"
|
||||
"subAnnounceDesc" = "VPNクライアントに表示されるお知らせのテキスト"
|
||||
"subEnableRouting" = "ルーティングを有効化"
|
||||
"subEnableRoutingDesc" = "VPNクライアントでルーティングを有効にするためのグローバル設定。(Happのみ)"
|
||||
"subRoutingRules" = "ルーティングルール"
|
||||
"subRoutingRulesDesc" = "VPNクライアントのグローバルルーティングルール。(Happのみ)"
|
||||
"subListen" = "監視IP"
|
||||
"subListenDesc" = "サブスクリプションサービスが監視するIPアドレス(空白にするとすべてのIPを監視)"
|
||||
"subPort" = "監視ポート"
|
||||
|
|
@ -450,6 +460,8 @@
|
|||
"FreedomStrategyDesc" = "Freedomプロトコル内のネットワークの出力戦略を設定する"
|
||||
"RoutingStrategy" = "ルーティングドメイン戦略設定"
|
||||
"RoutingStrategyDesc" = "DNS解決の全体的なルーティング戦略を設定する"
|
||||
"outboundTestUrl" = "アウトバウンドテスト URL"
|
||||
"outboundTestUrlDesc" = "アウトバウンド接続テストに使用する URL。既定値"
|
||||
"Torrent" = "BitTorrent プロトコルをブロック"
|
||||
"Inbounds" = "インバウンドルール"
|
||||
"InboundsDesc" = "特定のクライアントからのトラフィックを受け入れる"
|
||||
|
|
@ -531,6 +543,12 @@
|
|||
"psk" = "共有キー"
|
||||
"domainStrategy" = "ドメイン戦略"
|
||||
|
||||
[pages.xray.tun]
|
||||
"nameDesc" = "TUN インターフェースの名前。デフォルトは 'xray0' です"
|
||||
"mtuDesc" = "最大伝送単位。データパケットの最大サイズ。デフォルトは 1500 です"
|
||||
"userLevel" = "ユーザーレベル"
|
||||
"userLevelDesc" = "このインバウンドを通じて確立されたすべての接続は、このユーザーレベルを使用します。デフォルトは 0 です"
|
||||
|
||||
[pages.xray.dns]
|
||||
"enable" = "DNSを有効にする"
|
||||
"enableDesc" = "組み込みDNSサーバーを有効にする"
|
||||
|
|
|
|||
|
|
@ -374,6 +374,16 @@
|
|||
"subJsonEnable" = "Ativar/Desativar o endpoint de assinatura JSON de forma independente."
|
||||
"subTitle" = "Título da Assinatura"
|
||||
"subTitleDesc" = "Título exibido no cliente VPN"
|
||||
"subSupportUrl" = "URL de Suporte"
|
||||
"subSupportUrlDesc" = "Link de suporte técnico exibido no cliente VPN"
|
||||
"subProfileUrl" = "URL de Perfil"
|
||||
"subProfileUrlDesc" = "Um link para o seu site exibido no cliente VPN"
|
||||
"subAnnounce" = "Anúncio"
|
||||
"subAnnounceDesc" = "O texto do anúncio exibido no cliente VPN"
|
||||
"subEnableRouting" = "Ativar roteamento"
|
||||
"subEnableRoutingDesc" = "Configuração global para habilitar o roteamento no cliente VPN. (Apenas para Happ)"
|
||||
"subRoutingRules" = "Regras de roteamento"
|
||||
"subRoutingRulesDesc" = "Regras de roteamento globais para o cliente VPN. (Apenas para Happ)"
|
||||
"subListen" = "IP de Escuta"
|
||||
"subListenDesc" = "O endereço IP para o serviço de assinatura. (deixe em branco para escutar em todos os IPs)"
|
||||
"subPort" = "Porta de Escuta"
|
||||
|
|
@ -450,6 +460,8 @@
|
|||
"FreedomStrategyDesc" = "Definir a estratégia de saída para a rede no Protocolo Freedom."
|
||||
"RoutingStrategy" = "Estratégia Geral de Roteamento"
|
||||
"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"
|
||||
"Inbounds" = "Inbounds"
|
||||
"InboundsDesc" = "Aceitar clientes específicos."
|
||||
|
|
@ -531,6 +543,12 @@
|
|||
"psk" = "Chave Pré-Compartilhada"
|
||||
"domainStrategy" = "Estratégia de Domínio"
|
||||
|
||||
[pages.xray.tun]
|
||||
"nameDesc" = "O nome da interface TUN. O padrão é 'xray0'"
|
||||
"mtuDesc" = "Unidade Máxima de Transmissão. O tamanho máximo dos pacotes de dados. O padrão é 1500"
|
||||
"userLevel" = "Nível do Usuário"
|
||||
"userLevelDesc" = "Todas as conexões feitas através deste inbound usarão este nível de usuário. O padrão é 0"
|
||||
|
||||
[pages.xray.dns]
|
||||
"enable" = "Ativar DNS"
|
||||
"enableDesc" = "Ativar o servidor DNS integrado"
|
||||
|
|
|
|||
|
|
@ -373,7 +373,17 @@
|
|||
"subEnableDesc" = "Функция подписки с отдельной конфигурацией"
|
||||
"subJsonEnable" = "Включить/отключить JSON-эндпоинт подписки независимо."
|
||||
"subTitle" = "Заголовок подписки"
|
||||
"subTitleDesc" = "Название подписки, которое видит клиент в VPN клиенте"
|
||||
"subTitleDesc" = "Название подписки, которое видит клиент в VPN-клиенте"
|
||||
"subSupportUrl" = "URL поддержки"
|
||||
"subSupportUrlDesc" = "Ссылка на техническую поддержку, отображаемая в VPN-клиенте"
|
||||
"subProfileUrl" = "URL профиля"
|
||||
"subProfileUrlDesc" = "Ссылка на ваш сайт, отображаемая в VPN-клиенте"
|
||||
"subAnnounce" = "Объявление"
|
||||
"subAnnounceDesc" = "Текст объявления, отображаемый в VPN-клиенте"
|
||||
"subEnableRouting" = "Включить маршрутизацию"
|
||||
"subEnableRoutingDesc" = "Глобальная настройка для включения маршрутизации в VPN-клиенте. (Только для Happ)"
|
||||
"subRoutingRules" = "Правила маршрутизации"
|
||||
"subRoutingRulesDesc" = "Глобальные правила маршрутизации для VPN-клиента. (Только для Happ)"
|
||||
"subListen" = "Прослушивание IP"
|
||||
"subListenDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все IP-адреса"
|
||||
"subPort" = "Порт подписки"
|
||||
|
|
@ -450,6 +460,8 @@
|
|||
"FreedomStrategyDesc" = "Установка стратегии вывода сети в протоколе Freedom"
|
||||
"RoutingStrategy" = "Настройка маршрутизации доменов"
|
||||
"RoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS"
|
||||
"outboundTestUrl" = "URL для теста исходящего"
|
||||
"outboundTestUrlDesc" = "URL для проверки подключения исходящего"
|
||||
"Torrent" = "Заблокировать BitTorrent"
|
||||
"Inbounds" = "Входящие подключения"
|
||||
"InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных клиентов"
|
||||
|
|
@ -531,6 +543,12 @@
|
|||
"psk" = "Общий ключ"
|
||||
"domainStrategy" = "Стратегия домена"
|
||||
|
||||
[pages.xray.tun]
|
||||
"nameDesc" = "Имя интерфейса TUN. Значение по умолчанию - 'xray0'"
|
||||
"mtuDesc" = "Максимальная единица передачи. Максимальный размер пакетов данных. Значение по умолчанию - 1500"
|
||||
"userLevel" = "Уровень пользователя"
|
||||
"userLevelDesc" = "Все соединения, установленные через этот входящий поток, будут использовать этот уровень пользователя. Значение по умолчанию - 0"
|
||||
|
||||
[pages.xray.dns]
|
||||
"enable" = "Включить DNS"
|
||||
"enableDesc" = "Включить встроенный DNS-сервер"
|
||||
|
|
|
|||
|
|
@ -374,6 +374,16 @@
|
|||
"subJsonEnable" = "JSON abonelik uç noktasını bağımsız olarak Etkinleştir/Devre Dışı bırak."
|
||||
"subTitle" = "Abonelik Başlığı"
|
||||
"subTitleDesc" = "VPN istemcisinde gösterilen başlık"
|
||||
"subSupportUrl" = "Destek URL'si"
|
||||
"subSupportUrlDesc" = "VPN istemcisinde gösterilen teknik destek bağlantısı"
|
||||
"subProfileUrl" = "Profil URL'si"
|
||||
"subProfileUrlDesc" = "VPN istemcisinde görüntülenen web sitenize giden bağlantı"
|
||||
"subAnnounce" = "Duyuru"
|
||||
"subAnnounceDesc" = "VPN istemcisinde görüntülenen duyuru metni"
|
||||
"subEnableRouting" = "Yönlendirmeyi etkinleştir"
|
||||
"subEnableRoutingDesc" = "VPN istemcisinde yönlendirmeyi etkinleştirmek için genel ayar. (Yalnızca Happ için)"
|
||||
"subRoutingRules" = "Yönlendirme kuralları"
|
||||
"subRoutingRulesDesc" = "VPN istemcisi için genel yönlendirme kuralları. (Yalnızca Happ için)"
|
||||
"subListen" = "Dinleme IP"
|
||||
"subListenDesc" = "Abonelik hizmeti için IP adresi. (tüm IP'leri dinlemek için boş bırakın)"
|
||||
"subPort" = "Dinleme Portu"
|
||||
|
|
@ -450,6 +460,8 @@
|
|||
"FreedomStrategyDesc" = "Freedom Protokolünde ağın çıkış stratejisini ayarlayın."
|
||||
"RoutingStrategy" = "Genel Yönlendirme Stratejisi"
|
||||
"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"
|
||||
"Inbounds" = "Gelenler"
|
||||
"InboundsDesc" = "Belirli müşterileri kabul eder."
|
||||
|
|
@ -531,6 +543,12 @@
|
|||
"psk" = "Ön Paylaşılan Anahtar"
|
||||
"domainStrategy" = "Alan Adı Stratejisi"
|
||||
|
||||
[pages.xray.tun]
|
||||
"nameDesc" = "TUN arabiriminin adı. Varsayılan değer 'xray0'dir"
|
||||
"mtuDesc" = "Maksimum İletim Birimi. Veri paketlerinin maksimum boyutu. Varsayılan değer 1500'dür"
|
||||
"userLevel" = "Kullanıcı Seviyesi"
|
||||
"userLevelDesc" = "Bu giriş yoluyla yapılan tüm bağlantılar bu kullanıcı seviyesini kullanacaktır. Varsayılan değer 0'dır"
|
||||
|
||||
[pages.xray.dns]
|
||||
"enable" = "DNS'yi Etkinleştir"
|
||||
"enableDesc" = "Dahili DNS sunucusunu etkinleştir"
|
||||
|
|
|
|||
|
|
@ -374,6 +374,16 @@
|
|||
"subJsonEnable" = "Увімкнути/вимкнути JSON-кінець підписки незалежно."
|
||||
"subTitle" = "Назва Підписки"
|
||||
"subTitleDesc" = "Назва, яка відображається у VPN-клієнті"
|
||||
"subSupportUrl" = "URL підтримки"
|
||||
"subSupportUrlDesc" = "Посилання на технічну підтримку, що відображається у VPN-клієнті"
|
||||
"subProfileUrl" = "URL профілю"
|
||||
"subProfileUrlDesc" = "Посилання на ваш вебсайт, що відображається у VPN-клієнті"
|
||||
"subAnnounce" = "Оголошення"
|
||||
"subAnnounceDesc" = "Текст оголошення, що відображається у VPN-клієнті"
|
||||
"subEnableRouting" = "Увімкнути маршрутизацію"
|
||||
"subEnableRoutingDesc" = "Глобальне налаштування для увімкнення маршрутизації у VPN-клієнті. (Тільки для Happ)"
|
||||
"subRoutingRules" = "Правила маршрутизації"
|
||||
"subRoutingRulesDesc" = "Глобальні правила маршрутизації для VPN-клієнта. (Тільки для Happ)"
|
||||
"subListen" = "Слухати IP"
|
||||
"subListenDesc" = "IP-адреса для служби підписки. (залиште порожнім, щоб слухати всі IP-адреси)"
|
||||
"subPort" = "Слухати порт"
|
||||
|
|
@ -450,6 +460,8 @@
|
|||
"FreedomStrategyDesc" = "Установити стратегію виведення для мережі в протоколі свободи."
|
||||
"RoutingStrategy" = "Загальна стратегія маршрутизації"
|
||||
"RoutingStrategyDesc" = "Установити загальну стратегію маршрутизації трафіку для вирішення всіх запитів."
|
||||
"outboundTestUrl" = "URL тесту outbound"
|
||||
"outboundTestUrlDesc" = "URL для перевірки з'єднання outbound"
|
||||
"Torrent" = "Блокувати протокол BitTorrent"
|
||||
"Inbounds" = "Вхідні"
|
||||
"InboundsDesc" = "Прийняття певних клієнтів."
|
||||
|
|
@ -531,6 +543,12 @@
|
|||
"psk" = "Спільний ключ"
|
||||
"domainStrategy" = "Стратегія домену"
|
||||
|
||||
[pages.xray.tun]
|
||||
"nameDesc" = "Назва інтерфейсу TUN. Значення за замовчуванням - 'xray0'"
|
||||
"mtuDesc" = "Максимальна одиниця передачі. Максимальний розмір пакетів даних. Значення за замовчуванням - 1500"
|
||||
"userLevel" = "Рівень користувача"
|
||||
"userLevelDesc" = "Всі з'єднання, встановлені через цей вхід, використовуватимуть цей рівень користувача. Значення за замовчуванням - 0"
|
||||
|
||||
[pages.xray.dns]
|
||||
"enable" = "Увімкнути DNS"
|
||||
"enableDesc" = "Увімкнути вбудований DNS-сервер"
|
||||
|
|
|
|||
|
|
@ -374,6 +374,16 @@
|
|||
"subJsonEnable" = "Bật/Tắt điểm cuối đăng ký JSON độc lập."
|
||||
"subTitle" = "Tiêu đề Đăng ký"
|
||||
"subTitleDesc" = "Tiêu đề hiển thị trong ứng dụng VPN"
|
||||
"subSupportUrl" = "URL Hỗ trợ"
|
||||
"subSupportUrlDesc" = "Liên kết hỗ trợ kỹ thuật hiển thị trong ứng dụng VPN"
|
||||
"subProfileUrl" = "URL Hồ sơ"
|
||||
"subProfileUrlDesc" = "Liên kết đến trang web của bạn hiển thị trong ứng dụng VPN"
|
||||
"subAnnounce" = "Thông báo"
|
||||
"subAnnounceDesc" = "Văn bản thông báo hiển thị trong ứng dụng VPN"
|
||||
"subEnableRouting" = "Bật định tuyến"
|
||||
"subEnableRoutingDesc" = "Cài đặt toàn cục để bật định tuyến trong ứng dụng khách VPN. (Chỉ dành cho Happ)"
|
||||
"subRoutingRules" = "Quy tắc định tuyến"
|
||||
"subRoutingRulesDesc" = "Quy tắc định tuyến toàn cầu cho client VPN. (Chỉ dành cho Happ)"
|
||||
"subListen" = "Listening IP"
|
||||
"subListenDesc" = "Mặc định để trống để nghe tất cả các IP"
|
||||
"subPort" = "Cổng gói đăng ký"
|
||||
|
|
@ -450,6 +460,8 @@
|
|||
"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"
|
||||
"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"
|
||||
"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ể."
|
||||
|
|
@ -531,6 +543,12 @@
|
|||
"psk" = "Khóa chia sẻ"
|
||||
"domainStrategy" = "Chiến lược tên miền"
|
||||
|
||||
[pages.xray.tun]
|
||||
"nameDesc" = "Tên của giao diện TUN. Giá trị mặc định là 'xray0'"
|
||||
"mtuDesc" = "Đơn vị Truyền Tối đa. Kích thước tối đa của các gói dữ liệu. Giá trị mặc định là 1500"
|
||||
"userLevel" = "Mức Người Dùng"
|
||||
"userLevelDesc" = "Tất cả các kết nối được thực hiện thông qua inbound này sẽ sử dụng mức người dùng này. Giá trị mặc định là 0"
|
||||
|
||||
[pages.xray.dns]
|
||||
"enable" = "Kích hoạt DNS"
|
||||
"enableDesc" = "Kích hoạt máy chủ DNS tích hợp"
|
||||
|
|
|
|||
|
|
@ -374,6 +374,16 @@
|
|||
"subJsonEnable" = "单独启用/禁用 JSON 订阅端点。"
|
||||
"subTitle" = "订阅标题"
|
||||
"subTitleDesc" = "在VPN客户端中显示的标题"
|
||||
"subSupportUrl" = "支持链接"
|
||||
"subSupportUrlDesc" = "VPN 客户端中显示的技术支持链接"
|
||||
"subProfileUrl" = "个人资料链接"
|
||||
"subProfileUrlDesc" = "VPN 客户端中显示的网站链接"
|
||||
"subAnnounce" = "公告"
|
||||
"subAnnounceDesc" = "VPN 客户端中显示的公告文本"
|
||||
"subEnableRouting" = "启用路由"
|
||||
"subEnableRoutingDesc" = "在 VPN 客户端中启用路由的全局设置。(僅限 Happ)"
|
||||
"subRoutingRules" = "路由規則"
|
||||
"subRoutingRulesDesc" = "VPN 用戶端的全域路由規則。(僅限 Happ)"
|
||||
"subListen" = "监听 IP"
|
||||
"subListenDesc" = "订阅服务监听的 IP 地址(留空表示监听所有 IP)"
|
||||
"subPort" = "监听端口"
|
||||
|
|
@ -450,6 +460,8 @@
|
|||
"FreedomStrategyDesc" = "设置 Freedom 协议中网络的输出策略"
|
||||
"RoutingStrategy" = "配置路由域策略"
|
||||
"RoutingStrategyDesc" = "设置 DNS 解析的整体路由策略"
|
||||
"outboundTestUrl" = "出站测试 URL"
|
||||
"outboundTestUrlDesc" = "测试出站连接时使用的 URL"
|
||||
"Torrent" = "屏蔽 BitTorrent 协议"
|
||||
"Inbounds" = "入站规则"
|
||||
"InboundsDesc" = "接受来自特定客户端的流量"
|
||||
|
|
@ -531,6 +543,12 @@
|
|||
"psk" = "共享密钥"
|
||||
"domainStrategy" = "域策略"
|
||||
|
||||
[pages.xray.tun]
|
||||
"nameDesc" = "TUN 接口的名称。默认值为 'xray0'"
|
||||
"mtuDesc" = "最大传输单元。数据包的最大大小。默认值为 1500"
|
||||
"userLevel" = "用户级别"
|
||||
"userLevelDesc" = "通过此入站的所有连接都将使用此用户级别。默认值为 0"
|
||||
|
||||
[pages.xray.dns]
|
||||
"enable" = "启用 DNS"
|
||||
"enableDesc" = "启用内置 DNS 服务器"
|
||||
|
|
|
|||
|
|
@ -374,6 +374,16 @@
|
|||
"subJsonEnable" = "獨立啟用/停用 JSON 訂閱端點。"
|
||||
"subTitle" = "訂閱標題"
|
||||
"subTitleDesc" = "在VPN客戶端中顯示的標題"
|
||||
"subSupportUrl" = "支援連結"
|
||||
"subSupportUrlDesc" = "VPN 用戶端中顯示的技術支援連結"
|
||||
"subProfileUrl" = "個人資料連結"
|
||||
"subProfileUrlDesc" = "VPN 用戶端中顯示的網站連結"
|
||||
"subAnnounce" = "公告"
|
||||
"subAnnounceDesc" = "VPN 用戶端中顯示的公告文字"
|
||||
"subEnableRouting" = "啟用路由"
|
||||
"subEnableRoutingDesc" = "在 VPN 用戶端中啟用路由的全域設定。(僅限 Happ)"
|
||||
"subRoutingRules" = "路由規則"
|
||||
"subRoutingRulesDesc" = "VPN 用戶端的全域路由規則。(僅限 Happ)"
|
||||
"subListen" = "監聽 IP"
|
||||
"subListenDesc" = "訂閱服務監聽的 IP 地址(留空表示監聽所有 IP)"
|
||||
"subPort" = "監聽埠"
|
||||
|
|
@ -450,6 +460,8 @@
|
|||
"FreedomStrategyDesc" = "設定 Freedom 協議中網路的輸出策略"
|
||||
"RoutingStrategy" = "配置路由域策略"
|
||||
"RoutingStrategyDesc" = "設定 DNS 解析的整體路由策略"
|
||||
"outboundTestUrl" = "出站測試 URL"
|
||||
"outboundTestUrlDesc" = "測試出站連線時使用的 URL"
|
||||
"Torrent" = "遮蔽 BitTorrent 協議"
|
||||
"Inbounds" = "入站規則"
|
||||
"InboundsDesc" = "接受來自特定客戶端的流量"
|
||||
|
|
@ -531,6 +543,12 @@
|
|||
"psk" = "共享金鑰"
|
||||
"domainStrategy" = "域策略"
|
||||
|
||||
[pages.xray.tun]
|
||||
"nameDesc" = "TUN 介面的名稱。預設值為 'xray0'"
|
||||
"mtuDesc" = "最大傳輸單元。資料包的最大大小。預設值為 1500"
|
||||
"userLevel" = "用戶級別"
|
||||
"userLevelDesc" = "通過此入站的所有連接都將使用此用戶級別。預設值為 0"
|
||||
|
||||
[pages.xray.dns]
|
||||
"enable" = "啟用 DNS"
|
||||
"enableDesc" = "啟用內建 DNS 伺服器"
|
||||
|
|
|
|||
|
|
@ -487,6 +487,6 @@ func (s *Server) GetCron() *cron.Cron {
|
|||
}
|
||||
|
||||
// GetWSHub returns the WebSocket hub instance.
|
||||
func (s *Server) GetWSHub() interface{} {
|
||||
func (s *Server) GetWSHub() any {
|
||||
return s.wsHub
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,12 +20,13 @@ const (
|
|||
MessageTypeInbounds MessageType = "inbounds" // Inbounds list update
|
||||
MessageTypeNotification MessageType = "notification" // System notification
|
||||
MessageTypeXrayState MessageType = "xray_state" // Xray state change
|
||||
MessageTypeOutbounds MessageType = "outbounds" // Outbounds list update
|
||||
)
|
||||
|
||||
// Message represents a WebSocket message
|
||||
type Message struct {
|
||||
Type MessageType `json:"type"`
|
||||
Payload interface{} `json:"payload"`
|
||||
Payload any `json:"payload"`
|
||||
Time int64 `json:"time"`
|
||||
}
|
||||
|
||||
|
|
@ -249,7 +250,7 @@ func (h *Hub) broadcastParallel(clients []*Client, message []byte) {
|
|||
}
|
||||
|
||||
// Broadcast sends a message to all connected clients
|
||||
func (h *Hub) Broadcast(messageType MessageType, payload interface{}) {
|
||||
func (h *Hub) Broadcast(messageType MessageType, payload any) {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -288,7 +289,7 @@ func (h *Hub) Broadcast(messageType MessageType, payload interface{}) {
|
|||
}
|
||||
|
||||
// BroadcastToTopic sends a message only to clients subscribed to the specific topic
|
||||
func (h *Hub) BroadcastToTopic(messageType MessageType, payload interface{}) {
|
||||
func (h *Hub) BroadcastToTopic(messageType MessageType, payload any) {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ func GetHub() *Hub {
|
|||
}
|
||||
|
||||
// BroadcastStatus broadcasts server status update to all connected clients
|
||||
func BroadcastStatus(status interface{}) {
|
||||
func BroadcastStatus(status any) {
|
||||
hub := GetHub()
|
||||
if hub != nil {
|
||||
hub.Broadcast(MessageTypeStatus, status)
|
||||
|
|
@ -33,7 +33,7 @@ func BroadcastStatus(status interface{}) {
|
|||
}
|
||||
|
||||
// BroadcastTraffic broadcasts traffic statistics update to all connected clients
|
||||
func BroadcastTraffic(traffic interface{}) {
|
||||
func BroadcastTraffic(traffic any) {
|
||||
hub := GetHub()
|
||||
if hub != nil {
|
||||
hub.Broadcast(MessageTypeTraffic, traffic)
|
||||
|
|
@ -41,13 +41,21 @@ func BroadcastTraffic(traffic interface{}) {
|
|||
}
|
||||
|
||||
// BroadcastInbounds broadcasts inbounds list update to all connected clients
|
||||
func BroadcastInbounds(inbounds interface{}) {
|
||||
func BroadcastInbounds(inbounds any) {
|
||||
hub := GetHub()
|
||||
if hub != nil {
|
||||
hub.Broadcast(MessageTypeInbounds, inbounds)
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastOutbounds broadcasts outbounds list update to all connected clients
|
||||
func BroadcastOutbounds(outbounds any) {
|
||||
hub := GetHub()
|
||||
if hub != nil {
|
||||
hub.Broadcast(MessageTypeOutbounds, outbounds)
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastNotification broadcasts a system notification to all connected clients
|
||||
func BroadcastNotification(title, message, level string) {
|
||||
hub := GetHub()
|
||||
|
|
|
|||
Binary file not shown.
BIN
windows_files/SSL/Win64OpenSSL_Light-3_6_0.exe
Normal file
BIN
windows_files/SSL/Win64OpenSSL_Light-3_6_0.exe
Normal file
Binary file not shown.
16
x-ui.service.arch
Normal file
16
x-ui.service.arch
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[Unit]
|
||||
Description=x-ui Service
|
||||
After=network.target
|
||||
Wants=network.target
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=-/etc/conf.d/x-ui
|
||||
Environment="XRAY_VMESS_AEAD_FORCED=false"
|
||||
Type=simple
|
||||
WorkingDirectory=/usr/lib/x-ui/
|
||||
ExecStart=/usr/lib/x-ui/x-ui
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
397
x-ui.sh
397
x-ui.sh
|
|
@ -19,6 +19,23 @@ function LOGI() {
|
|||
echo -e "${green}[INF] $* ${plain}"
|
||||
}
|
||||
|
||||
# Port helpers: detect listener and owning process (best effort)
|
||||
is_port_in_use() {
|
||||
local port="$1"
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}'
|
||||
return
|
||||
fi
|
||||
if command -v netstat >/dev/null 2>&1; then
|
||||
netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}'
|
||||
return
|
||||
fi
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Simple helpers for domain/IP validation
|
||||
is_ipv4() {
|
||||
[[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1
|
||||
|
|
@ -30,7 +47,7 @@ is_ip() {
|
|||
is_ipv4 "$1" || is_ipv6 "$1"
|
||||
}
|
||||
is_domain() {
|
||||
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[A-Za-z]{2,}$ ]] && return 0 || return 1
|
||||
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1
|
||||
}
|
||||
|
||||
# check root
|
||||
|
|
@ -212,9 +229,9 @@ reset_user() {
|
|||
|
||||
read -rp "Do you want to disable currently configured two-factor authentication? (y/n): " twoFactorConfirm
|
||||
if [[ $twoFactorConfirm != "y" && $twoFactorConfirm != "Y" ]]; then
|
||||
${xui_folder}/x-ui setting -username ${config_account} -password ${config_password} -resetTwoFactor false >/dev/null 2>&1
|
||||
${xui_folder}/x-ui setting -username "${config_account}" -password "${config_password}" -resetTwoFactor false >/dev/null 2>&1
|
||||
else
|
||||
${xui_folder}/x-ui setting -username ${config_account} -password ${config_password} -resetTwoFactor true >/dev/null 2>&1
|
||||
${xui_folder}/x-ui setting -username "${config_account}" -password "${config_password}" -resetTwoFactor true >/dev/null 2>&1
|
||||
echo -e "Two factor authentication has been disabled."
|
||||
fi
|
||||
|
||||
|
|
@ -230,57 +247,6 @@ gen_random_string() {
|
|||
echo "$random_string"
|
||||
}
|
||||
|
||||
# Generate and configure a self-signed SSL certificate
|
||||
setup_self_signed_certificate() {
|
||||
local name="$1" # domain or IP to place in SAN
|
||||
local certDir="/root/cert/selfsigned"
|
||||
|
||||
LOGI "Generating a self-signed certificate (not publicly trusted)..."
|
||||
|
||||
mkdir -p "$certDir"
|
||||
|
||||
local sanExt=""
|
||||
if [[ "$name" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ || "$name" =~ : ]]; then
|
||||
sanExt="IP:${name}"
|
||||
else
|
||||
sanExt="DNS:${name}"
|
||||
fi
|
||||
|
||||
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
|
||||
-keyout "${certDir}/privkey.pem" \
|
||||
-out "${certDir}/fullchain.pem" \
|
||||
-subj "/CN=${name}" \
|
||||
-addext "subjectAltName=${sanExt}" >/dev/null 2>&1
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
local tmpCfg="${certDir}/openssl.cnf"
|
||||
cat > "$tmpCfg" <<EOF
|
||||
[req]
|
||||
distinguished_name=req_distinguished_name
|
||||
req_extensions=v3_req
|
||||
[req_distinguished_name]
|
||||
[v3_req]
|
||||
subjectAltName=${sanExt}
|
||||
EOF
|
||||
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
|
||||
-keyout "${certDir}/privkey.pem" \
|
||||
-out "${certDir}/fullchain.pem" \
|
||||
-subj "/CN=${name}" \
|
||||
-config "$tmpCfg" -extensions v3_req >/dev/null 2>&1
|
||||
rm -f "$tmpCfg"
|
||||
fi
|
||||
|
||||
if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then
|
||||
LOGE "Failed to generate self-signed certificate"
|
||||
return 1
|
||||
fi
|
||||
|
||||
chmod 755 ${certDir}/* >/dev/null 2>&1
|
||||
${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" >/dev/null 2>&1
|
||||
LOGI "Self-signed certificate configured. Browsers will show a warning."
|
||||
return 0
|
||||
}
|
||||
|
||||
reset_webbasepath() {
|
||||
echo -e "${yellow}Resetting Web Base Path${plain}"
|
||||
|
||||
|
|
@ -340,16 +306,19 @@ check_config() {
|
|||
fi
|
||||
else
|
||||
echo -e "${red}⚠ WARNING: No SSL certificate configured!${plain}"
|
||||
read -rp "Generate a self-signed SSL certificate now? [y/N]: " gen_self
|
||||
if [[ "$gen_self" == "y" || "$gen_self" == "Y" ]]; then
|
||||
echo -e "${yellow}You can get a Let's Encrypt certificate for your IP address (valid ~6 days, auto-renews).${plain}"
|
||||
read -rp "Generate SSL certificate for IP now? [y/N]: " gen_ssl
|
||||
if [[ "$gen_ssl" == "y" || "$gen_ssl" == "Y" ]]; then
|
||||
stop >/dev/null 2>&1
|
||||
setup_self_signed_certificate "${server_ip}"
|
||||
ssl_cert_issue_for_ip
|
||||
if [[ $? -eq 0 ]]; then
|
||||
restart >/dev/null 2>&1
|
||||
echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}"
|
||||
# ssl_cert_issue_for_ip already restarts the panel, but ensure it's running
|
||||
start >/dev/null 2>&1
|
||||
else
|
||||
LOGE "Self-signed SSL setup failed."
|
||||
LOGE "IP certificate setup failed."
|
||||
echo -e "${yellow}You can try again via option 18 (SSL Certificate Management).${plain}"
|
||||
start >/dev/null 2>&1
|
||||
fi
|
||||
else
|
||||
echo -e "${yellow}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}"
|
||||
|
|
@ -561,20 +530,27 @@ bbr_menu() {
|
|||
|
||||
disable_bbr() {
|
||||
|
||||
if ! grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf || ! grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then
|
||||
if [[ $(sysctl -n net.ipv4.tcp_congestion_control) != "bbr" ]] || [[ ! $(sysctl -n net.core.default_qdisc) =~ ^(fq|cake)$ ]]; then
|
||||
echo -e "${yellow}BBR is not currently enabled.${plain}"
|
||||
before_show_menu
|
||||
fi
|
||||
|
||||
# Replace BBR with CUBIC configurations
|
||||
sed -i 's/net.core.default_qdisc=fq/net.core.default_qdisc=pfifo_fast/' /etc/sysctl.conf
|
||||
sed -i 's/net.ipv4.tcp_congestion_control=bbr/net.ipv4.tcp_congestion_control=cubic/' /etc/sysctl.conf
|
||||
if [ -f "/etc/sysctl.d/99-bbr-x-ui.conf" ]; then
|
||||
old_settings=$(head -1 /etc/sysctl.d/99-bbr-x-ui.conf | tr -d '#')
|
||||
sysctl -w net.core.default_qdisc="${old_settings%:*}"
|
||||
sysctl -w net.ipv4.tcp_congestion_control="${old_settings#*:}"
|
||||
rm /etc/sysctl.d/99-bbr-x-ui.conf
|
||||
sysctl --system
|
||||
else
|
||||
# Replace BBR with CUBIC configurations
|
||||
if [ -f "/etc/sysctl.conf" ]; then
|
||||
sed -i 's/net.core.default_qdisc=fq/net.core.default_qdisc=pfifo_fast/' /etc/sysctl.conf
|
||||
sed -i 's/net.ipv4.tcp_congestion_control=bbr/net.ipv4.tcp_congestion_control=cubic/' /etc/sysctl.conf
|
||||
sysctl -p
|
||||
fi
|
||||
fi
|
||||
|
||||
# Apply changes
|
||||
sysctl -p
|
||||
|
||||
# Verify that BBR is replaced with CUBIC
|
||||
if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "cubic" ]]; then
|
||||
if [[ $(sysctl -n net.ipv4.tcp_congestion_control) != "bbr" ]]; then
|
||||
echo -e "${green}BBR has been replaced with CUBIC successfully.${plain}"
|
||||
else
|
||||
echo -e "${red}Failed to replace BBR with CUBIC. Please check your system configuration.${plain}"
|
||||
|
|
@ -582,50 +558,34 @@ disable_bbr() {
|
|||
}
|
||||
|
||||
enable_bbr() {
|
||||
if grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf && grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then
|
||||
if [[ $(sysctl -n net.ipv4.tcp_congestion_control) == "bbr" ]] && [[ $(sysctl -n net.core.default_qdisc) =~ ^(fq|cake)$ ]]; then
|
||||
echo -e "${green}BBR is already enabled!${plain}"
|
||||
before_show_menu
|
||||
fi
|
||||
|
||||
# Check the OS and install necessary packages
|
||||
case "${release}" in
|
||||
ubuntu | debian | armbian)
|
||||
apt-get update && apt-get install -yqq --no-install-recommends ca-certificates
|
||||
;;
|
||||
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
|
||||
dnf -y update && dnf -y install ca-certificates
|
||||
;;
|
||||
centos)
|
||||
if [[ "${VERSION_ID}" =~ ^7 ]]; then
|
||||
yum -y update && yum -y install ca-certificates
|
||||
else
|
||||
dnf -y update && dnf -y install ca-certificates
|
||||
fi
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
pacman -Sy --noconfirm ca-certificates
|
||||
;;
|
||||
opensuse-tumbleweed | opensuse-leap)
|
||||
zypper refresh && zypper -q install -y ca-certificates
|
||||
;;
|
||||
alpine)
|
||||
apk add ca-certificates
|
||||
;;
|
||||
*)
|
||||
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Enable BBR
|
||||
echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf
|
||||
echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf
|
||||
|
||||
# Apply changes
|
||||
sysctl -p
|
||||
if [ -d "/etc/sysctl.d/" ]; then
|
||||
{
|
||||
echo "#$(sysctl -n net.core.default_qdisc):$(sysctl -n net.ipv4.tcp_congestion_control)"
|
||||
echo "net.core.default_qdisc = fq"
|
||||
echo "net.ipv4.tcp_congestion_control = bbr"
|
||||
} > "/etc/sysctl.d/99-bbr-x-ui.conf"
|
||||
if [ -f "/etc/sysctl.conf" ]; then
|
||||
# Backup old settings from sysctl.conf, if any
|
||||
sed -i 's/^net.core.default_qdisc/# &/' /etc/sysctl.conf
|
||||
sed -i 's/^net.ipv4.tcp_congestion_control/# &/' /etc/sysctl.conf
|
||||
fi
|
||||
sysctl --system
|
||||
else
|
||||
sed -i '/net.core.default_qdisc/d' /etc/sysctl.conf
|
||||
sed -i '/net.ipv4.tcp_congestion_control/d' /etc/sysctl.conf
|
||||
echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf
|
||||
echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf
|
||||
sysctl -p
|
||||
fi
|
||||
|
||||
# Verify that BBR is enabled
|
||||
if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "bbr" ]]; then
|
||||
if [[ $(sysctl -n net.ipv4.tcp_congestion_control) == "bbr" ]]; then
|
||||
echo -e "${green}BBR has been enabled successfully.${plain}"
|
||||
else
|
||||
echo -e "${red}Failed to enable BBR. Please check your system configuration.${plain}"
|
||||
|
|
@ -951,24 +911,23 @@ delete_ports() {
|
|||
}
|
||||
|
||||
update_all_geofiles() {
|
||||
update_main_geofiles
|
||||
update_ir_geofiles
|
||||
update_ru_geofiles
|
||||
update_geofiles "main"
|
||||
update_geofiles "IR"
|
||||
update_geofiles "RU"
|
||||
}
|
||||
|
||||
update_main_geofiles() {
|
||||
curl -fLRo geoip.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
curl -fLRo geosite.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
}
|
||||
|
||||
update_ir_geofiles() {
|
||||
curl -fLRo geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
|
||||
curl -fLRo geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
|
||||
}
|
||||
|
||||
update_ru_geofiles() {
|
||||
curl -fLRo geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
curl -fLRo geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
update_geofiles() {
|
||||
case "${1}" in
|
||||
"main") dat_files=(geoip geosite); dat_source="Loyalsoldier/v2ray-rules-dat";;
|
||||
"IR") dat_files=(geoip_IR geosite_IR); dat_source="chocolate4u/Iran-v2ray-rules" ;;
|
||||
"RU") dat_files=(geoip_RU geosite_RU); dat_source="runetfreedom/russia-v2ray-rules-dat";;
|
||||
esac
|
||||
for dat in "${dat_files[@]}"; do
|
||||
# Remove suffix for remote filename (e.g., geoip_IR -> geoip)
|
||||
remote_file="${dat%%_*}"
|
||||
curl -fLRo ${xui_folder}/bin/${dat}.dat -z ${xui_folder}/bin/${dat}.dat \
|
||||
https://github.com/${dat_source}/releases/latest/download/${remote_file}.dat
|
||||
done
|
||||
}
|
||||
|
||||
update_geo() {
|
||||
|
|
@ -979,24 +938,22 @@ update_geo() {
|
|||
echo -e "${green}\t0.${plain} Back to Main Menu"
|
||||
read -rp "Choose an option: " choice
|
||||
|
||||
cd ${xui_folder}/bin
|
||||
|
||||
case "$choice" in
|
||||
0)
|
||||
show_menu
|
||||
;;
|
||||
1)
|
||||
update_main_geofiles
|
||||
update_geofiles "main"
|
||||
echo -e "${green}Loyalsoldier datasets have been updated successfully!${plain}"
|
||||
restart
|
||||
;;
|
||||
2)
|
||||
update_ir_geofiles
|
||||
update_geofiles "IR"
|
||||
echo -e "${green}chocolate4u datasets have been updated successfully!${plain}"
|
||||
restart
|
||||
;;
|
||||
3)
|
||||
update_ru_geofiles
|
||||
update_geofiles "RU"
|
||||
echo -e "${green}runetfreedom datasets have been updated successfully!${plain}"
|
||||
restart
|
||||
;;
|
||||
|
|
@ -1036,12 +993,12 @@ install_acme() {
|
|||
}
|
||||
|
||||
ssl_cert_issue_main() {
|
||||
echo -e "${green}\t1.${plain} Get SSL"
|
||||
echo -e "${green}\t1.${plain} Get SSL (Domain)"
|
||||
echo -e "${green}\t2.${plain} Revoke"
|
||||
echo -e "${green}\t3.${plain} Force Renew"
|
||||
echo -e "${green}\t4.${plain} Show Existing Domains"
|
||||
echo -e "${green}\t5.${plain} Set Cert paths for the panel"
|
||||
echo -e "${green}\t6.${plain} Auto SSL for Server IP"
|
||||
echo -e "${green}\t6.${plain} Get SSL for IP Address (6-day cert, auto-renews)"
|
||||
echo -e "${green}\t0.${plain} Back to Main Menu"
|
||||
|
||||
read -rp "Choose an option: " choice
|
||||
|
|
@ -1136,9 +1093,10 @@ ssl_cert_issue_main() {
|
|||
ssl_cert_issue_main
|
||||
;;
|
||||
6)
|
||||
echo -e "${yellow}Automatic SSL Certificate for Server IP${plain}"
|
||||
echo -e "This will automatically obtain and configure an SSL certificate for your server's IP address."
|
||||
echo -e "${yellow}Note: Let's Encrypt supports IP certificates. Make sure port 80 is open.${plain}"
|
||||
echo -e "${yellow}Let's Encrypt SSL Certificate for IP Address${plain}"
|
||||
echo -e "This will obtain a certificate for your server's IP using the shortlived profile."
|
||||
echo -e "${yellow}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}"
|
||||
echo -e "${yellow}Port 80 must be open and accessible from the internet.${plain}"
|
||||
confirm "Do you want to proceed?" "y"
|
||||
if [[ $? == 0 ]]; then
|
||||
ssl_cert_issue_for_ip
|
||||
|
|
@ -1155,6 +1113,7 @@ ssl_cert_issue_main() {
|
|||
|
||||
ssl_cert_issue_for_ip() {
|
||||
LOGI "Starting automatic SSL certificate generation for server IP..."
|
||||
LOGI "Using Let's Encrypt shortlived profile (~6 days validity, auto-renews)"
|
||||
|
||||
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')
|
||||
local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
|
||||
|
|
@ -1172,6 +1131,11 @@ ssl_cert_issue_for_ip() {
|
|||
|
||||
LOGI "Server IP detected: ${server_ip}"
|
||||
|
||||
# Ask for optional IPv6
|
||||
local ipv6_addr=""
|
||||
read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr
|
||||
ipv6_addr="${ipv6_addr// /}" # Trim whitespace
|
||||
|
||||
# check for acme.sh first
|
||||
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||
LOGI "acme.sh not found, installing..."
|
||||
|
|
@ -1211,66 +1175,114 @@ ssl_cert_issue_for_ip() {
|
|||
;;
|
||||
esac
|
||||
|
||||
# check if certificate already exists for this IP
|
||||
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
|
||||
if [ "${currentCert}" == "${server_ip}" ]; then
|
||||
LOGI "Certificate already exists for IP: ${server_ip}"
|
||||
certPath="/root/cert/${server_ip}"
|
||||
else
|
||||
# create directory for certificate
|
||||
certPath="/root/cert/${server_ip}"
|
||||
if [ ! -d "$certPath" ]; then
|
||||
mkdir -p "$certPath"
|
||||
else
|
||||
rm -rf "$certPath"
|
||||
mkdir -p "$certPath"
|
||||
fi
|
||||
|
||||
# Use port 80 for certificate issuance
|
||||
local WebPort=80
|
||||
LOGI "Using port ${WebPort} to issue certificate for IP: ${server_ip}"
|
||||
LOGI "Make sure port ${WebPort} is open and not in use..."
|
||||
|
||||
# issue the certificate for IP
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
|
||||
~/.acme.sh/acme.sh --issue -d ${server_ip} --listen-v6 --standalone --httpport ${WebPort} --force
|
||||
if [ $? -ne 0 ]; then
|
||||
LOGE "Failed to issue certificate for IP: ${server_ip}"
|
||||
LOGE "Make sure port ${WebPort} is open and the server is accessible from the internet"
|
||||
rm -rf ~/.acme.sh/${server_ip}
|
||||
return 1
|
||||
else
|
||||
LOGI "Certificate issued successfully for IP: ${server_ip}"
|
||||
fi
|
||||
|
||||
# install the certificate
|
||||
~/.acme.sh/acme.sh --installcert -d ${server_ip} \
|
||||
--key-file /root/cert/${server_ip}/privkey.pem \
|
||||
--fullchain-file /root/cert/${server_ip}/fullchain.pem \
|
||||
--reloadcmd "x-ui restart"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
LOGE "Failed to install certificate"
|
||||
rm -rf ~/.acme.sh/${server_ip}
|
||||
return 1
|
||||
else
|
||||
LOGI "Certificate installed successfully"
|
||||
fi
|
||||
|
||||
# enable auto-renew
|
||||
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
|
||||
chmod 755 $certPath/*
|
||||
# Create certificate directory
|
||||
certPath="/root/cert/ip"
|
||||
mkdir -p "$certPath"
|
||||
|
||||
# Build domain arguments
|
||||
local domain_args="-d ${server_ip}"
|
||||
if [[ -n "$ipv6_addr" ]] && is_ipv6 "$ipv6_addr"; then
|
||||
domain_args="${domain_args} -d ${ipv6_addr}"
|
||||
LOGI "Including IPv6 address: ${ipv6_addr}"
|
||||
fi
|
||||
|
||||
# Choose port for HTTP-01 listener (default 80, allow override)
|
||||
local WebPort=""
|
||||
read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort
|
||||
WebPort="${WebPort:-80}"
|
||||
if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then
|
||||
LOGE "Invalid port provided. Falling back to 80."
|
||||
WebPort=80
|
||||
fi
|
||||
LOGI "Using port ${WebPort} to issue certificate for IP: ${server_ip}"
|
||||
if [[ "${WebPort}" -ne 80 ]]; then
|
||||
LOGI "Reminder: Let's Encrypt still reaches port 80; forward external port 80 to ${WebPort} for validation."
|
||||
fi
|
||||
|
||||
while true; do
|
||||
if is_port_in_use "${WebPort}"; then
|
||||
LOGI "Port ${WebPort} is currently in use."
|
||||
|
||||
local alt_port=""
|
||||
read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port
|
||||
alt_port="${alt_port// /}"
|
||||
if [[ -z "${alt_port}" ]]; then
|
||||
LOGE "Port ${WebPort} is busy; cannot proceed with issuance."
|
||||
return 1
|
||||
fi
|
||||
if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then
|
||||
LOGE "Invalid port provided."
|
||||
return 1
|
||||
fi
|
||||
WebPort="${alt_port}"
|
||||
continue
|
||||
else
|
||||
LOGI "Port ${WebPort} is free and ready for standalone validation."
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Reload command - restarts panel after renewal
|
||||
local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null"
|
||||
|
||||
# issue the certificate for IP with shortlived profile
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force
|
||||
~/.acme.sh/acme.sh --issue \
|
||||
${domain_args} \
|
||||
--standalone \
|
||||
--server letsencrypt \
|
||||
--certificate-profile shortlived \
|
||||
--days 6 \
|
||||
--httpport ${WebPort} \
|
||||
--force
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
LOGE "Failed to issue certificate for IP: ${server_ip}"
|
||||
LOGE "Make sure port ${WebPort} is open and the server is accessible from the internet"
|
||||
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
|
||||
rm -rf ~/.acme.sh/${server_ip} 2>/dev/null
|
||||
[[ -n "$ipv6_addr" ]] && rm -rf ~/.acme.sh/${ipv6_addr} 2>/dev/null
|
||||
rm -rf ${certPath} 2>/dev/null
|
||||
return 1
|
||||
else
|
||||
LOGI "Certificate issued successfully for IP: ${server_ip}"
|
||||
fi
|
||||
|
||||
# Install the certificate
|
||||
# Note: acme.sh may report "Reload error" and exit non-zero if reloadcmd fails,
|
||||
# but the cert files are still installed. We check for files instead of exit code.
|
||||
~/.acme.sh/acme.sh --installcert -d ${server_ip} \
|
||||
--key-file "${certPath}/privkey.pem" \
|
||||
--fullchain-file "${certPath}/fullchain.pem" \
|
||||
--reloadcmd "${reloadCmd}" 2>&1 || true
|
||||
|
||||
# Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero)
|
||||
if [[ ! -f "${certPath}/fullchain.pem" || ! -f "${certPath}/privkey.pem" ]]; then
|
||||
LOGE "Certificate files not found after installation"
|
||||
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
|
||||
rm -rf ~/.acme.sh/${server_ip} 2>/dev/null
|
||||
[[ -n "$ipv6_addr" ]] && rm -rf ~/.acme.sh/${ipv6_addr} 2>/dev/null
|
||||
rm -rf ${certPath} 2>/dev/null
|
||||
return 1
|
||||
fi
|
||||
|
||||
LOGI "Certificate files installed successfully"
|
||||
|
||||
# enable auto-renew
|
||||
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
|
||||
chmod 600 $certPath/privkey.pem 2>/dev/null
|
||||
chmod 644 $certPath/fullchain.pem 2>/dev/null
|
||||
|
||||
# Set certificate paths for the panel
|
||||
local webCertFile="/root/cert/${server_ip}/fullchain.pem"
|
||||
local webKeyFile="/root/cert/${server_ip}/privkey.pem"
|
||||
local webCertFile="${certPath}/fullchain.pem"
|
||||
local webKeyFile="${certPath}/privkey.pem"
|
||||
|
||||
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
|
||||
${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile"
|
||||
LOGI "Certificate configured for panel"
|
||||
LOGI " - Certificate File: $webCertFile"
|
||||
LOGI " - Private Key File: $webKeyFile"
|
||||
LOGI " - Validity: ~6 days (auto-renews via acme.sh cron)"
|
||||
echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}"
|
||||
LOGI "Panel will restart to apply SSL certificate..."
|
||||
restart
|
||||
|
|
@ -1379,7 +1391,7 @@ ssl_cert_issue() {
|
|||
LOGI "Will use port: ${WebPort} to issue certificates. Please make sure this port is open."
|
||||
|
||||
# issue the certificate
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force
|
||||
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force
|
||||
if [ $? -ne 0 ]; then
|
||||
LOGE "Issuing certificate failed, please check logs."
|
||||
|
|
@ -1433,12 +1445,14 @@ ssl_cert_issue() {
|
|||
if [ $? -ne 0 ]; then
|
||||
LOGE "Auto renew failed, certificate details:"
|
||||
ls -lah cert/*
|
||||
chmod 755 $certPath/*
|
||||
chmod 600 $certPath/privkey.pem
|
||||
chmod 644 $certPath/fullchain.pem
|
||||
exit 1
|
||||
else
|
||||
LOGI "Auto renew succeeded, certificate details:"
|
||||
ls -lah cert/*
|
||||
chmod 755 $certPath/*
|
||||
chmod 600 $certPath/privkey.pem
|
||||
chmod 644 $certPath/fullchain.pem
|
||||
fi
|
||||
|
||||
# Prompt user to set panel paths after successful certificate installation
|
||||
|
|
@ -1504,7 +1518,7 @@ ssl_cert_issue_CF() {
|
|||
LOGD "Your registered email address is: ${CF_AccountEmail}"
|
||||
|
||||
# Set the default CA to Let's Encrypt
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force
|
||||
if [ $? -ne 0 ]; then
|
||||
LOGE "Default CA, Let'sEncrypt fail, script exiting..."
|
||||
exit 1
|
||||
|
|
@ -1578,7 +1592,8 @@ ssl_cert_issue_CF() {
|
|||
else
|
||||
LOGI "The certificate is installed and auto-renewal is turned on. Specific information is as follows:"
|
||||
ls -lah ${certPath}/*
|
||||
chmod 755 ${certPath}/*
|
||||
chmod 600 ${certPath}/privkey.pem
|
||||
chmod 644 ${certPath}/fullchain.pem
|
||||
fi
|
||||
|
||||
# Prompt user to set panel paths after successful certificate installation
|
||||
|
|
@ -2047,11 +2062,15 @@ SSH_port_forwarding() {
|
|||
)
|
||||
local server_ip=""
|
||||
for ip_address in "${URL_lists[@]}"; do
|
||||
server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
|
||||
if [[ -n "${server_ip}" ]]; then
|
||||
local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null)
|
||||
local http_code=$(echo "$response" | tail -n1)
|
||||
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
|
||||
if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
|
||||
server_ip="${ip_result}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
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_listenIP=$(${xui_folder}/x-ui setting -getListen true | grep -Eo 'listenIP: .+' | awk '{print $2}')
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]an
|
|||
}
|
||||
// Add testseed if provided
|
||||
if testseedVal, ok := user["testseed"]; ok {
|
||||
if testseedArr, ok := testseedVal.([]interface{}); ok && len(testseedArr) >= 4 {
|
||||
if testseedArr, ok := testseedVal.([]any); ok && len(testseedArr) >= 4 {
|
||||
testseed := make([]uint32, len(testseedArr))
|
||||
for i, v := range testseedArr {
|
||||
if num, ok := v.(float64); ok {
|
||||
|
|
|
|||
|
|
@ -110,6 +110,15 @@ func NewProcess(xrayConfig *Config) *Process {
|
|||
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 {
|
||||
cmd *exec.Cmd
|
||||
|
||||
|
|
@ -118,10 +127,11 @@ type process struct {
|
|||
|
||||
onlineClients []string
|
||||
|
||||
config *Config
|
||||
logWriter *LogWriter
|
||||
exitErr error
|
||||
startTime time.Time
|
||||
config *Config
|
||||
configPath string // if set, use this path instead of GetConfigPath() and remove on Stop
|
||||
logWriter *LogWriter
|
||||
exitErr error
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
// newProcess creates a new internal process struct for Xray.
|
||||
|
|
@ -134,6 +144,13 @@ 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.
|
||||
func (p *process) IsRunning() bool {
|
||||
if p.cmd == nil || p.cmd.Process == nil {
|
||||
|
|
@ -238,6 +255,9 @@ func (p *process) Start() (err error) {
|
|||
}
|
||||
|
||||
configPath := GetConfigPath()
|
||||
if p.configPath != "" {
|
||||
configPath = p.configPath
|
||||
}
|
||||
err = os.WriteFile(configPath, data, fs.ModePerm)
|
||||
if err != nil {
|
||||
return common.NewErrorf("Failed to write configuration file: %v", err)
|
||||
|
|
@ -278,6 +298,16 @@ func (p *process) Stop() error {
|
|||
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" {
|
||||
return p.cmd.Process.Kill()
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in a new issue