diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..89d23d69 --- /dev/null +++ b/.github/copilot-instructions.md @@ -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 diff --git a/.github/workflows/cleanup_caches.yml b/.github/workflows/cleanup_caches.yml new file mode 100644 index 00000000..dcf50fce --- /dev/null +++ b/.github/workflows/cleanup_caches.yml @@ -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 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a17271e7..462c1228 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,7 +93,7 @@ jobs: cd x-ui/bin # Download dependencies - Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v26.1.31/" + Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v26.2.2/" if [ "${{ matrix.platform }}" == "amd64" ]; then wget -q ${Xray_URL}Xray-linux-64.zip unzip Xray-linux-64.zip @@ -177,21 +177,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/v26.1.31/" + $Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v26.2.2/" 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" diff --git a/DockerInit.sh b/DockerInit.sh index d8acad77..6f153efe 100755 --- a/DockerInit.sh +++ b/DockerInit.sh @@ -27,7 +27,7 @@ case $1 in esac mkdir -p build/bin cd build/bin -curl -sfLRO "https://github.com/XTLS/Xray-core/releases/download/v26.1.31/Xray-linux-${ARCH}.zip" +curl -sfLRO "https://github.com/XTLS/Xray-core/releases/download/v26.2.2/Xray-linux-${ARCH}.zip" unzip "Xray-linux-${ARCH}.zip" rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat mv xray "xray-linux-${FNAME}" diff --git a/go.mod b/go.mod index 4f031534..98f44d28 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/valyala/fasthttp v1.69.0 github.com/xlzd/gotp v0.1.0 - github.com/xtls/xray-core v1.260131.0 + github.com/xtls/xray-core v1.260202.0 go.uber.org/atomic v1.11.0 golang.org/x/crypto v0.47.0 golang.org/x/sys v0.40.0 @@ -40,7 +40,7 @@ require ( github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudwego/base64x v0.1.6 // 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 diff --git a/go.sum b/go.sum index 59e6c7f3..247ac90d 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,8 @@ 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/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= @@ -195,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.260131.0 h1:gPBykLhUvRZ8sfubNerkwWqV3c15UtmSYQG2cgKqrV4= -github.com/xtls/xray-core v1.260131.0/go.mod h1:cxzYFZrxu1B1NtPjHsqv4UzgDvRA71mV4rXYH4KtO7Q= +github.com/xtls/xray-core v1.260202.0 h1:dYduYxGlkn/krSQJbmksbTtCdRe8OFb3YwpuXXEJG5c= +github.com/xtls/xray-core v1.260202.0/go.mod h1:cxzYFZrxu1B1NtPjHsqv4UzgDvRA71mV4rXYH4KtO7Q= 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= diff --git a/sub/subController.go b/sub/subController.go index 7653a4e1..53b5580b 100644 --- a/sub/subController.go +++ b/sub/subController.go @@ -3,8 +3,8 @@ package sub import ( "encoding/base64" "fmt" - "strings" "strconv" + "strings" "github.com/mhsanaei/3x-ui/v2/config" @@ -64,8 +64,8 @@ func NewSUBController( subEncrypt: encrypt, updateInterval: update, - subService: sub, - subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub), + subService: sub, + subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub), } a.initRouter(g) return a @@ -170,13 +170,13 @@ func (a *SUBController) subJsons(c *gin.Context) { // 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, + c *gin.Context, + header, + updateInterval, + profileTitle string, profileSupportUrl string, - profileUrl string, - profileAnnounce string, + profileUrl string, + profileAnnounce string, profileEnableRouting bool, profileRoutingRules string, ) { diff --git a/web/assets/js/model/inbound.js b/web/assets/js/model/inbound.js index 3f3f8831..9bc93c30 100644 --- a/web/assets/js/model/inbound.js +++ b/web/assets/js/model/inbound.js @@ -967,7 +967,7 @@ class SockoptStreamSettings extends XrayCommonClass { } } -class FinalMask extends XrayCommonClass { +class UdpMask extends XrayCommonClass { constructor(type = 'salamander', settings = {}) { super(); this.type = type; @@ -982,6 +982,8 @@ class FinalMask extends XrayCommonClass { 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': @@ -995,20 +997,35 @@ class FinalMask extends XrayCommonClass { } static fromJson(json = {}) { - return new FinalMask( + return new UdpMask( json.type || 'salamander', json.settings || {} ); } toJson() { - const result = { - type: this.type + return { + type: this.type, + settings: (this.settings && Object.keys(this.settings).length > 0) ? this.settings : undefined }; - if (this.settings && Object.keys(this.settings).length > 0) { - result.settings = this.settings; - } - return result; + } +} + +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()) + }; + } } @@ -1024,7 +1041,7 @@ class StreamSettings extends XrayCommonClass { grpcSettings = new GrpcStreamSettings(), httpupgradeSettings = new HTTPUpgradeStreamSettings(), xhttpSettings = new xHTTPStreamSettings(), - finalmask = { udp: [] }, + finalmask = new FinalMaskStreamSettings(), sockopt = undefined, ) { super(); @@ -1044,10 +1061,7 @@ class StreamSettings extends XrayCommonClass { } addUdpMask(type = 'salamander') { - if (!this.finalmask.udp) { - this.finalmask.udp = []; - } - this.finalmask.udp.push(new FinalMask(type)); + this.finalmask.udp.push(new UdpMask(type)); } delUdpMask(index) { @@ -1056,6 +1070,10 @@ class StreamSettings extends XrayCommonClass { } } + get hasFinalMask() { + return this.finalmask.udp && this.finalmask.udp.length > 0; + } + get isTls() { return this.security === "tls"; } @@ -1090,14 +1108,6 @@ class StreamSettings extends XrayCommonClass { } static fromJson(json = {}) { - let finalmask = { udp: [] }; - if (json.finalmask) { - if (Array.isArray(json.finalmask)) { - finalmask.udp = json.finalmask.map(mask => FinalMask.fromJson(mask)); - } else if (json.finalmask.udp) { - finalmask.udp = json.finalmask.udp.map(mask => FinalMask.fromJson(mask)); - } - } return new StreamSettings( json.network, json.security, @@ -1110,7 +1120,7 @@ class StreamSettings extends XrayCommonClass { GrpcStreamSettings.fromJson(json.grpcSettings), HTTPUpgradeStreamSettings.fromJson(json.httpupgradeSettings), xHTTPStreamSettings.fromJson(json.xhttpSettings), - finalmask, + FinalMaskStreamSettings.fromJson(json.finalmask), SockoptStreamSettings.fromJson(json.sockopt), ); } @@ -1129,9 +1139,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.finalmask.udp && this.finalmask.udp.length > 0) ? { - udp: this.finalmask.udp.map(mask => mask.toJson()) - } : undefined, + finalmask: this.hasFinalMask ? this.finalmask.toJson() : undefined, sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined, }; } @@ -1302,14 +1310,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; } @@ -1386,8 +1386,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; @@ -1450,8 +1448,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; @@ -1555,8 +1551,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; @@ -1636,8 +1630,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; diff --git a/web/assets/js/model/outbound.js b/web/assets/js/model/outbound.js index 3e0dd0d4..fc110b4e 100644 --- a/web/assets/js/model/outbound.js +++ b/web/assets/js/model/outbound.js @@ -568,7 +568,7 @@ class SockoptStreamSettings extends CommonClass { } } -class FinalMask extends CommonClass { +class UdpMask extends CommonClass { constructor(type = 'salamander', settings = {}) { super(); this.type = type; @@ -596,21 +596,35 @@ class FinalMask extends CommonClass { } static fromJson(json = {}) { - return new FinalMask( + return new UdpMask( json.type || 'salamander', json.settings || {} ); } toJson() { - const result = { - type: this.type + return { + type: this.type, + settings: (this.settings && Object.keys(this.settings).length > 0) ? this.settings : undefined }; - // Only include settings if they exist and are not empty - if (this.settings && Object.keys(this.settings).length > 0) { - result.settings = this.settings; - } - return result; + } +} + +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()) + }; + } } @@ -627,7 +641,7 @@ class StreamSettings extends CommonClass { httpupgradeSettings = new HttpUpgradeStreamSettings(), xhttpSettings = new xHTTPStreamSettings(), hysteriaSettings = new HysteriaStreamSettings(), - finalmask = { udp: [] }, + finalmask = new FinalMaskStreamSettings(), sockopt = undefined, ) { super(); @@ -647,10 +661,7 @@ class StreamSettings extends CommonClass { } addUdpMask(type = 'salamander') { - if (!this.finalmask.udp) { - this.finalmask.udp = []; - } - this.finalmask.udp.push(new FinalMask(type)); + this.finalmask.udp.push(new UdpMask(type)); } delUdpMask(index) { @@ -659,6 +670,10 @@ class StreamSettings extends CommonClass { } } + get hasFinalMask() { + return this.finalmask.udp && this.finalmask.udp.length > 0; + } + get isTls() { return this.security === 'tls'; } @@ -676,16 +691,6 @@ class StreamSettings extends CommonClass { } static fromJson(json = {}) { - let finalmask = { udp: [] }; - if (json.finalmask) { - if (Array.isArray(json.finalmask)) { - // Legacy format: direct array (backward compatibility) - finalmask.udp = json.finalmask.map(mask => FinalMask.fromJson(mask)); - } else if (json.finalmask.udp) { - // New format: object with udp array - finalmask.udp = json.finalmask.udp.map(mask => FinalMask.fromJson(mask)); - } - } return new StreamSettings( json.network, json.security, @@ -698,7 +703,7 @@ class StreamSettings extends CommonClass { HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings), xHTTPStreamSettings.fromJson(json.xhttpSettings), HysteriaStreamSettings.fromJson(json.hysteriaSettings), - finalmask, + FinalMaskStreamSettings.fromJson(json.finalmask), SockoptStreamSettings.fromJson(json.sockopt), ); } @@ -717,9 +722,7 @@ class StreamSettings extends CommonClass { httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined, xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined, hysteriaSettings: network === 'hysteria' ? this.hysteria.toJson() : undefined, - finalmask: (this.finalmask.udp && this.finalmask.udp.length > 0) ? { - udp: this.finalmask.udp.map(mask => mask.toJson()) - } : undefined, + finalmask: this.hasFinalMask ? this.finalmask.toJson() : undefined, sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined, }; } diff --git a/web/html/form/stream/stream_finalmask.html b/web/html/form/stream/stream_finalmask.html index 4ed7d6a1..35962dfa 100644 --- a/web/html/form/stream/stream_finalmask.html +++ b/web/html/form/stream/stream_finalmask.html @@ -45,6 +45,9 @@ mKCP Original + + xICMP (Experimental) + + + + + + + diff --git a/web/html/modals/inbound_info_modal.html b/web/html/modals/inbound_info_modal.html index 72023e75..1ab187ee 100644 --- a/web/html/modals/inbound_info_modal.html +++ b/web/html/modals/inbound_info_modal.html @@ -1,5 +1,8 @@ {{define "modals/inboundInfoModal"}} - + @@ -26,7 +29,8 @@
-