Browse Source

Merge branch 'main' into bash

Sanaei 4 hours ago
parent
commit
b5cb069a07

+ 5 - 0
config/config.go

@@ -57,6 +57,11 @@ func IsDebug() bool {
 	return os.Getenv("XUI_DEBUG") == "true"
 }
 
+// IsSkipHSTS returns true if skipping HSTS mode is enabled via the XUI_SKIP_HSTS environment variable.
+func IsSkipHSTS() bool {
+	return os.Getenv("XUI_SKIP_HSTS") == "true"
+}
+
 // GetBinFolderPath returns the path to the binary folder, defaulting to "bin" if not set via XUI_BIN_FOLDER.
 func GetBinFolderPath() string {
 	binFolderPath := os.Getenv("XUI_BIN_FOLDER")

+ 34 - 0
database/db.go

@@ -11,6 +11,7 @@ import (
 	"os"
 	"path"
 	"slices"
+	"strconv"
 	"strings"
 	"time"
 
@@ -198,6 +199,36 @@ func runSeeders(isUsersEmpty bool) error {
 	return nil
 }
 
+// normalizeClientJSONFields coerces loosely-typed numeric fields in a raw
+// settings.clients entry so json.Unmarshal into model.Client doesn't fail
+// when older rows wrote tgId/limitIp/totalGB/etc. as strings. Empty strings
+// drop the key so the field falls back to its zero value.
+func normalizeClientJSONFields(obj map[string]any) {
+	normalizeInt := func(key string) {
+		raw, exists := obj[key]
+		if !exists {
+			return
+		}
+		s, ok := raw.(string)
+		if !ok {
+			return
+		}
+		trimmed := strings.ReplaceAll(strings.TrimSpace(s), " ", "")
+		if trimmed == "" {
+			delete(obj, key)
+			return
+		}
+		if n, err := strconv.ParseInt(trimmed, 10, 64); err == nil {
+			obj[key] = n
+		} else {
+			delete(obj, key)
+		}
+	}
+	for _, k := range []string{"tgId", "limitIp", "totalGB", "expiryTime", "reset", "created_at", "updated_at"} {
+		normalizeInt(k)
+	}
+}
+
 func seedClientsFromInboundJSON() error {
 	var inbounds []model.Inbound
 	if err := db.Find(&inbounds).Error; err != nil {
@@ -226,12 +257,15 @@ func seedClientsFromInboundJSON() error {
 				if !ok {
 					continue
 				}
+				normalizeClientJSONFields(obj)
 				blob, err := json.Marshal(obj)
 				if err != nil {
 					continue
 				}
 				var c model.Client
 				if err := json.Unmarshal(blob, &c); err != nil {
+					log.Printf("ClientsTable seed: skip client in inbound %d (unmarshal failed): %v; payload=%s",
+						inbound.Id, err, string(blob))
 					continue
 				}
 				email := strings.TrimSpace(c.Email)

+ 77 - 107
frontend/package-lock.json

@@ -27,7 +27,7 @@
         "eslint": "^10.3.0",
         "eslint-plugin-vue": "^10.9.1",
         "globals": "^17.6.0",
-        "vite": "^8.0.11",
+        "vite": "^8.0.14",
         "vue-eslint-parser": "^10.4.0"
       },
       "engines": {
@@ -610,9 +610,9 @@
       }
     },
     "node_modules/@oxc-project/types": {
-      "version": "0.130.0",
-      "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz",
-      "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
+      "version": "0.132.0",
+      "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz",
+      "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==",
       "dev": true,
       "license": "MIT",
       "funding": {
@@ -620,9 +620,9 @@
       }
     },
     "node_modules/@rolldown/binding-android-arm64": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
-      "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz",
+      "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==",
       "cpu": [
         "arm64"
       ],
@@ -637,9 +637,9 @@
       }
     },
     "node_modules/@rolldown/binding-darwin-arm64": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
-      "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz",
+      "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==",
       "cpu": [
         "arm64"
       ],
@@ -654,9 +654,9 @@
       }
     },
     "node_modules/@rolldown/binding-darwin-x64": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
-      "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz",
+      "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==",
       "cpu": [
         "x64"
       ],
@@ -671,9 +671,9 @@
       }
     },
     "node_modules/@rolldown/binding-freebsd-x64": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
-      "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz",
+      "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==",
       "cpu": [
         "x64"
       ],
@@ -688,9 +688,9 @@
       }
     },
     "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
-      "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz",
+      "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==",
       "cpu": [
         "arm"
       ],
@@ -705,16 +705,13 @@
       }
     },
     "node_modules/@rolldown/binding-linux-arm64-gnu": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
-      "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz",
+      "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==",
       "cpu": [
         "arm64"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -725,16 +722,13 @@
       }
     },
     "node_modules/@rolldown/binding-linux-arm64-musl": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
-      "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz",
+      "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==",
       "cpu": [
         "arm64"
       ],
       "dev": true,
-      "libc": [
-        "musl"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -745,16 +739,13 @@
       }
     },
     "node_modules/@rolldown/binding-linux-ppc64-gnu": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
-      "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz",
+      "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==",
       "cpu": [
         "ppc64"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -765,16 +756,13 @@
       }
     },
     "node_modules/@rolldown/binding-linux-s390x-gnu": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
-      "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz",
+      "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==",
       "cpu": [
         "s390x"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -785,16 +773,13 @@
       }
     },
     "node_modules/@rolldown/binding-linux-x64-gnu": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
-      "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz",
+      "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==",
       "cpu": [
         "x64"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -805,16 +790,13 @@
       }
     },
     "node_modules/@rolldown/binding-linux-x64-musl": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
-      "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz",
+      "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==",
       "cpu": [
         "x64"
       ],
       "dev": true,
-      "libc": [
-        "musl"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -825,9 +807,9 @@
       }
     },
     "node_modules/@rolldown/binding-openharmony-arm64": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
-      "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz",
+      "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==",
       "cpu": [
         "arm64"
       ],
@@ -842,9 +824,9 @@
       }
     },
     "node_modules/@rolldown/binding-wasm32-wasi": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
-      "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz",
+      "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==",
       "cpu": [
         "wasm32"
       ],
@@ -861,9 +843,9 @@
       }
     },
     "node_modules/@rolldown/binding-win32-arm64-msvc": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
-      "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz",
+      "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==",
       "cpu": [
         "arm64"
       ],
@@ -878,9 +860,9 @@
       }
     },
     "node_modules/@rolldown/binding-win32-x64-msvc": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
-      "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz",
+      "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==",
       "cpu": [
         "x64"
       ],
@@ -2202,9 +2184,6 @@
         "arm64"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -2226,9 +2205,6 @@
         "arm64"
       ],
       "dev": true,
-      "libc": [
-        "musl"
-      ],
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -2250,9 +2226,6 @@
         "x64"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -2274,9 +2247,6 @@
         "x64"
       ],
       "dev": true,
-      "libc": [
-        "musl"
-      ],
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -2623,9 +2593,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.5.14",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
-      "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
+      "version": "8.5.15",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+      "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
       "funding": [
         {
           "type": "opencollective",
@@ -2642,7 +2612,7 @@
       ],
       "license": "MIT",
       "dependencies": {
-        "nanoid": "^3.3.11",
+        "nanoid": "^3.3.12",
         "picocolors": "^1.1.1",
         "source-map-js": "^1.2.1"
       },
@@ -2715,13 +2685,13 @@
       "license": "MIT"
     },
     "node_modules/rolldown": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
-      "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz",
+      "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@oxc-project/types": "=0.130.0",
+        "@oxc-project/types": "=0.132.0",
         "@rolldown/pluginutils": "^1.0.0"
       },
       "bin": {
@@ -2731,21 +2701,21 @@
         "node": "^20.19.0 || >=22.12.0"
       },
       "optionalDependencies": {
-        "@rolldown/binding-android-arm64": "1.0.1",
-        "@rolldown/binding-darwin-arm64": "1.0.1",
-        "@rolldown/binding-darwin-x64": "1.0.1",
-        "@rolldown/binding-freebsd-x64": "1.0.1",
-        "@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
-        "@rolldown/binding-linux-arm64-gnu": "1.0.1",
-        "@rolldown/binding-linux-arm64-musl": "1.0.1",
-        "@rolldown/binding-linux-ppc64-gnu": "1.0.1",
-        "@rolldown/binding-linux-s390x-gnu": "1.0.1",
-        "@rolldown/binding-linux-x64-gnu": "1.0.1",
-        "@rolldown/binding-linux-x64-musl": "1.0.1",
-        "@rolldown/binding-openharmony-arm64": "1.0.1",
-        "@rolldown/binding-wasm32-wasi": "1.0.1",
-        "@rolldown/binding-win32-arm64-msvc": "1.0.1",
-        "@rolldown/binding-win32-x64-msvc": "1.0.1"
+        "@rolldown/binding-android-arm64": "1.0.2",
+        "@rolldown/binding-darwin-arm64": "1.0.2",
+        "@rolldown/binding-darwin-x64": "1.0.2",
+        "@rolldown/binding-freebsd-x64": "1.0.2",
+        "@rolldown/binding-linux-arm-gnueabihf": "1.0.2",
+        "@rolldown/binding-linux-arm64-gnu": "1.0.2",
+        "@rolldown/binding-linux-arm64-musl": "1.0.2",
+        "@rolldown/binding-linux-ppc64-gnu": "1.0.2",
+        "@rolldown/binding-linux-s390x-gnu": "1.0.2",
+        "@rolldown/binding-linux-x64-gnu": "1.0.2",
+        "@rolldown/binding-linux-x64-musl": "1.0.2",
+        "@rolldown/binding-openharmony-arm64": "1.0.2",
+        "@rolldown/binding-wasm32-wasi": "1.0.2",
+        "@rolldown/binding-win32-arm64-msvc": "1.0.2",
+        "@rolldown/binding-win32-x64-msvc": "1.0.2"
       }
     },
     "node_modules/scroll-into-view-if-needed": {
@@ -2957,16 +2927,16 @@
       "license": "MIT"
     },
     "node_modules/vite": {
-      "version": "8.0.13",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
-      "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
+      "version": "8.0.14",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz",
+      "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "lightningcss": "^1.32.0",
         "picomatch": "^4.0.4",
-        "postcss": "^8.5.14",
-        "rolldown": "1.0.1",
+        "postcss": "^8.5.15",
+        "rolldown": "1.0.2",
         "tinyglobby": "^0.2.16"
       },
       "bin": {

+ 1 - 1
frontend/package.json

@@ -34,7 +34,7 @@
     "eslint": "^10.3.0",
     "eslint-plugin-vue": "^10.9.1",
     "globals": "^17.6.0",
-    "vite": "^8.0.11",
+    "vite": "^8.0.14",
     "vue-eslint-parser": "^10.4.0"
   },
   "overrides": {

+ 70 - 8
frontend/src/models/outbound.js

@@ -748,6 +748,9 @@ export class SockoptStreamSettings extends CommonClass {
         penetrate = false,
         addressPortStrategy = Address_Port_Strategy.NONE,
         trustedXForwardedFor = [],
+        mark = 0,            
+        interfaceName = "",  
+
     ) {
         super();
         this.dialerProxy = dialerProxy;
@@ -757,6 +760,9 @@ export class SockoptStreamSettings extends CommonClass {
         this.penetrate = penetrate;
         this.addressPortStrategy = addressPortStrategy;
         this.trustedXForwardedFor = trustedXForwardedFor;
+        this.mark = mark;          
+        this.interfaceName = interfaceName; 
+
     }
 
     static fromJson(json = {}) {
@@ -768,7 +774,9 @@ export class SockoptStreamSettings extends CommonClass {
             json.tcpMptcp,
             json.penetrate,
             json.addressPortStrategy,
-            json.trustedXForwardedFor || []
+            json.trustedXForwardedFor || [],
+            json.mark ?? 0,      
+            json.interface ?? "", 
         );
     }
 
@@ -779,7 +787,9 @@ export class SockoptStreamSettings extends CommonClass {
             tcpKeepAliveInterval: this.tcpKeepAliveInterval,
             tcpMptcp: this.tcpMptcp,
             penetrate: this.penetrate,
-            addressPortStrategy: this.addressPortStrategy
+            addressPortStrategy: this.addressPortStrategy,
+            mark: this.mark, 
+            interface: this.interfaceName, 
         };
         if (this.trustedXForwardedFor && this.trustedXForwardedFor.length > 0) {
             result.trustedXForwardedFor = this.trustedXForwardedFor;
@@ -1138,8 +1148,12 @@ export class StreamSettings extends CommonClass {
     }
 
     static fromJson(json = {}) {
+        // Xray-core supports both "xhttpSettings" and "splithttpSettings" (backward-compat alias)
+        const xhttpJson = json.xhttpSettings ?? json.splithttpSettings;
+        // Normalize "splithttp" network name to "xhttp" for internal consistency
+        const network = json.network === 'splithttp' ? 'xhttp' : json.network;
         return new StreamSettings(
-            json.network,
+            network,
             json.security,
             TlsStreamSettings.fromJson(json.tlsSettings),
             RealityStreamSettings.fromJson(json.realitySettings),
@@ -1148,7 +1162,7 @@ export class StreamSettings extends CommonClass {
             WsStreamSettings.fromJson(json.wsSettings),
             GrpcStreamSettings.fromJson(json.grpcSettings),
             HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
-            xHTTPStreamSettings.fromJson(json.xhttpSettings),
+            xHTTPStreamSettings.fromJson(xhttpJson),
             HysteriaStreamSettings.fromJson(json.hysteriaSettings),
             FinalMaskStreamSettings.fromJson(json.finalmask),
             SockoptStreamSettings.fromJson(json.sockopt),
@@ -1379,12 +1393,28 @@ export class Outbound extends CommonClass {
         } else if (network === 'httpupgrade') {
             stream.httpupgrade = new HttpUpgradeStreamSettings(json.path, json.host);
         } else if (network === 'xhttp') {
-            // xHTTPStreamSettings positional args are (path, host, headers, ..., mode);
-            // passing `json.mode` as the 3rd argument used to land in the `headers`
-            // slot, dropping the mode on the floor. Build the object and set mode
-            // explicitly to avoid that.
             const xh = new xHTTPStreamSettings(json.path, json.host);
             if (json.mode) xh.mode = json.mode;
+            if (json.type && !json.mode) xh.mode = json.type;
+            // Padding / obfuscation — sing-box families use x_padding_bytes,
+            // while the extra block carries xPaddingBytes.
+            if (json.x_padding_bytes && !json.xPaddingBytes) json.xPaddingBytes = json.x_padding_bytes;
+            if (typeof json.xPaddingBytes === 'string' && json.xPaddingBytes) xh.xPaddingBytes = json.xPaddingBytes;
+            if (json.xPaddingObfsMode === true) {
+                xh.xPaddingObfsMode = true;
+                ["xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"].forEach(k => {
+                    if (typeof json[k] === 'string' && json[k]) xh[k] = json[k];
+                });
+            }
+            // Bidirectional string fields carried in the extra block
+            const xFields = ["sessionPlacement", "sessionKey", "seqPlacement", "seqKey", "uplinkDataPlacement", "uplinkDataKey", "scMaxEachPostBytes"];
+            xFields.forEach(k => {
+                if (typeof json[k] === 'string' && json[k]) xh[k] = json[k];
+            });
+            // Headers — VMess extra emits them as a {name: value} map
+            if (json.headers && typeof json.headers === 'object' && !Array.isArray(json.headers)) {
+                xh.headers = Object.entries(json.headers).map(([name, value]) => ({ name, value }));
+            }
             stream.xhttp = xh;
         }
 
@@ -1455,6 +1485,16 @@ export class Outbound extends CommonClass {
                     ["xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"].forEach(k => {
                         if (typeof extra[k] === 'string' && extra[k]) xh[k] = extra[k];
                     });
+                    if (!xh.mode && typeof extra.mode === 'string' && extra.mode) xh.mode = extra.mode;
+                    // Bidirectional string fields carried inside the extra block
+                    const xFields = ["sessionPlacement", "sessionKey", "seqPlacement", "seqKey", "uplinkDataPlacement", "uplinkDataKey", "scMaxEachPostBytes"];
+                    xFields.forEach(k => {
+                        if (typeof extra[k] === 'string' && extra[k]) xh[k] = extra[k];
+                    });
+                    // Headers — extra emits them as a {name: value} map
+                    if (extra.headers && typeof extra.headers === 'object' && !Array.isArray(extra.headers)) {
+                        xh.headers = Object.entries(extra.headers).map(([name, value]) => ({ name, value }));
+                    }
                 } catch (_) { /* ignore malformed extra */ }
             }
             stream.xhttp = xh;
@@ -1997,6 +2037,28 @@ Outbound.VLESSSettings = class extends CommonClass {
     }
 
     static fromJson(json = {}) {
+        // Handle v2rayN-style nested vnext array (standard Xray JSON format)
+        if (!ObjectUtil.isArrEmpty(json.vnext)) {
+            const v = json.vnext[0] || {};
+            const u = ObjectUtil.isArrEmpty(v.users) ? {} : v.users[0];
+            const saved = json.testseed;
+            const testseed = (Array.isArray(saved)
+                && saved.length === 4
+                && saved.every(v => Number.isInteger(v) && v > 0))
+                ? saved
+                : [];
+            return new Outbound.VLESSSettings(
+                v.address,
+                v.port,
+                u.id,
+                u.flow,
+                u.encryption,
+                json.reverse?.tag || '',
+                ReverseSniffing.fromJson(json.reverse?.sniffing || {}),
+                json.testpre || 0,
+                testseed,
+            );
+        }
         if (ObjectUtil.isEmpty(json.address) || ObjectUtil.isEmpty(json.port)) return new Outbound.VLESSSettings();
         const saved = json.testseed;
         const testseed = (Array.isArray(saved)

+ 42 - 11
frontend/src/pages/clients/ClientsPage.vue

@@ -43,6 +43,7 @@ const {
   tgBotEnable,
   expireDiff,
   trafficDiff,
+  pageSize,
   create,
   update,
   remove,
@@ -442,6 +443,10 @@ function expiryColor(row) {
 const sortState = ref({ column: null, order: null });
 const paginationState = ref({ current: 1, pageSize: 20 });
 
+watch(pageSize, (next) => {
+  if (next > 0) paginationState.value.pageSize = next;
+}, { immediate: true });
+
 function sortableCol(col, key) {
   return {
     ...col,
@@ -670,8 +675,9 @@ const columns = computed(() => [
                     </a-select>
                   </div>
 
-                  <a-table v-if="!isMobile" :columns="columns" :data-source="sortedClients" :loading="loading" row-key="email"
-                    :row-selection="rowSelection" :pagination="tablePagination" size="small" @change="onTableChange">
+                  <a-table v-if="!isMobile" :columns="columns" :data-source="sortedClients" :loading="loading"
+                    row-key="email" :row-selection="rowSelection" :pagination="tablePagination" size="small"
+                    @change="onTableChange">
                     <template #bodyCell="{ column, record }">
                       <template v-if="column.key === 'email'">
                         <div class="email-cell">
@@ -842,6 +848,11 @@ const columns = computed(() => [
   background: var(--bg-page);
 }
 
+.clients-page :deep(.ant-pagination-options-size-changer),
+.clients-page :deep(.ant-pagination-options-size-changer .ant-select-selector) {
+  min-width: 100px !important;
+}
+
 .clients-page.is-dark {
   --bg-page: #1e1e1e;
   --bg-card: #252526;
@@ -874,7 +885,7 @@ const columns = computed(() => [
   margin-bottom: 8px;
 }
 
-.filter-bar.mobile > * {
+.filter-bar.mobile>* {
   flex: 0 0 auto;
 }
 
@@ -911,11 +922,25 @@ const columns = computed(() => [
   vertical-align: middle;
 }
 
-.dot-green { background: #52c41a; }
-.dot-blue { background: #1677ff; }
-.dot-red { background: #ff4d4f; }
-.dot-orange { background: #fa8c16; }
-.dot-gray { background: rgba(128, 128, 128, 0.6); }
+.dot-green {
+  background: #52c41a;
+}
+
+.dot-blue {
+  background: #1677ff;
+}
+
+.dot-red {
+  background: #ff4d4f;
+}
+
+.dot-orange {
+  background: #fa8c16;
+}
+
+.dot-gray {
+  background: rgba(128, 128, 128, 0.6);
+}
 
 .status-tag {
   margin: 0 0 0 4px;
@@ -1050,8 +1075,6 @@ const columns = computed(() => [
 </style>
 
 <style>
-/* AD-Vue popovers teleport their content to <body>, so scoped styles
-   don't reach them — this block has to be unscoped. */
 .client-email-list {
   max-height: 280px;
   min-width: 160px;
@@ -1059,9 +1082,17 @@ const columns = computed(() => [
   padding-right: 4px;
 }
 
-.client-email-list > div {
+.client-email-list>div {
   padding: 2px 0;
   font-size: 12px;
   white-space: nowrap;
 }
+
+.ant-select-dropdown:has(.ant-select-item-option[title$="/ page"]) {
+  min-width: 110px !important;
+}
+
+.ant-select-dropdown:has(.ant-select-item-option[title$="/ page"]) .ant-select-item-option-content {
+  white-space: nowrap;
+}
 </style>

+ 3 - 0
frontend/src/pages/clients/useClients.js

@@ -14,6 +14,7 @@ export function useClients() {
   const tgBotEnable = ref(false);
   const expireDiff = ref(0);
   const trafficDiff = ref(0);
+  const pageSize = ref(0);
 
   async function refresh() {
     loading.value = true;
@@ -48,6 +49,7 @@ export function useClients() {
     tgBotEnable.value = !!s.tgBotEnable;
     expireDiff.value = (s.expireDiff ?? 0) * 86400000;
     trafficDiff.value = (s.trafficDiff ?? 0) * 1073741824;
+    pageSize.value = s.pageSize ?? 0;
   }
 
   async function create(payload) {
@@ -199,6 +201,7 @@ export function useClients() {
     tgBotEnable,
     expireDiff,
     trafficDiff,
+    pageSize,
     refresh,
     create,
     update,

+ 119 - 27
frontend/src/pages/inbounds/InboundInfoModal.vue

@@ -19,41 +19,20 @@ import { useDatepicker } from '@/composables/useDatepicker.js';
 const { t } = useI18n();
 const { datepicker } = useDatepicker();
 
-// One modal handles every protocol's info / share view because the
-// legacy template did the same. The big v-if forks at the top decide
-// which sub-block of the body renders:
-//   • multi-user inbound (VMess/VLess/Trojan/SS-multi/Hysteria) → per-
-//     client row + share links
-//   • SS single-user → connection details + share link
-//   • WireGuard → secret/peers + per-peer config download
-//   • Mixed/HTTP/Tunnel → connection details only
-//
-// We display links via QrPanel — each link gets its own QR + copy +
-// (for WireGuard configs) download button.
-
 const props = defineProps({
   open: { type: Boolean, default: false },
-  // Result of inbounds-page checkFallback() so the link-gen sees the
-  // root inbound's listen/port/security when the dbInbound is a
-  // domain-socket fallback (`@<name>`).
   dbInbound: { type: Object, default: null },
-  // Index into inbound.clients to focus on for multi-user inbounds.
   clientIndex: { type: Number, default: 0 },
-  // Sidecar config the legacy panel keyed off `app.*`.
   remarkModel: { type: String, default: '-ieo' },
   expireDiff: { type: Number, default: 0 },
   trafficDiff: { type: Number, default: 0 },
   ipLimitEnable: { type: Boolean, default: false },
   tgBotEnable: { type: Boolean, default: false },
-  // Address of the node hosting this inbound; '' for local. Wired
-  // through to share/QR link generation so node-managed inbounds
-  // produce links that connect to the node, not the central panel.
   nodeAddress: { type: String, default: '' },
   subSettings: {
     type: Object,
     default: () => ({ enable: false, subURI: '', subJsonURI: '', subJsonEnable: false }),
   },
-  // Email -> ts (last-online unix-ms) map fetched at the page level.
   lastOnlineMap: { type: Object, default: () => ({}) },
 });
 
@@ -598,7 +577,8 @@ const showSubscriptionTab = computed(
             <div v-if="inbound.settings.gateway?.length" class="info-row">
               <dt>Gateway</dt>
               <dd><a-tag v-for="(ip, j) in inbound.settings.gateway" :key="`tun-i-gw-${j}`" color="green"
-                  class="value-tag">{{ ip }}</a-tag></dd>
+                  class="value-tag">{{
+                  ip }}</a-tag></dd>
             </div>
             <div v-if="inbound.settings.dns?.length" class="info-row">
               <dt>DNS</dt>
@@ -612,7 +592,8 @@ const showSubscriptionTab = computed(
             <div v-if="inbound.settings.autoSystemRoutingTable?.length" class="info-row">
               <dt>Auto system routes</dt>
               <dd><a-tag v-for="(cidr, j) in inbound.settings.autoSystemRoutingTable" :key="`tun-i-rt-${j}`"
-                  color="green">{{ cidr }}</a-tag></dd>
+                  color="green">{{
+                  cidr }}</a-tag></dd>
             </div>
           </dl>
 
@@ -670,12 +651,101 @@ const showSubscriptionTab = computed(
                   <span class="account-sep">:</span>
                   <a-tag class="value-tag">{{ account.pass }}</a-tag>
                   <a-tooltip :title="t('copy')">
-                    <a-button size="small" @click="copyText(`${account.user}:${account.pass}`)">
-                      <template #icon>
-                        <CopyOutlined />
-                      </template>
+                    <a-button size="small" type="text"
+                      @click="copyText(`${account.user}:${account.pass}`)">
+                      <template #icon><CopyOutlined /></template>
                     </a-button>
                   </a-tooltip>
+                  <a-space :size="4" wrap class="share-buttons share-desktop">
+                    <a-tooltip :title="`socks5://${dbInbound.address}:${dbInbound.port}@${account.user}:${account.pass}`">
+                      <a-button size="small"
+                        @click="copyText(`socks5://${dbInbound.address}:${dbInbound.port}@${account.user}:${account.pass}`)">
+                        SOCKS5
+                      </a-button>
+                    </a-tooltip>
+                    <a-tooltip :title="`http://${dbInbound.address}:${dbInbound.port}@${account.user}:${account.pass}`">
+                      <a-button size="small"
+                        @click="copyText(`http://${dbInbound.address}:${dbInbound.port}@${account.user}:${account.pass}`)">
+                        HTTP
+                      </a-button>
+                    </a-tooltip>
+                    <a-tooltip title="https://t.me/socks?server=...&port=...&user=...&pass=...">
+                      <a-button size="small"
+                        @click="copyText(`https://t.me/socks?server=${encodeURIComponent(dbInbound.address)}&port=${dbInbound.port}&user=${encodeURIComponent(account.user)}&pass=${encodeURIComponent(account.pass)}`)">
+                        Telegram
+                      </a-button>
+                    </a-tooltip>
+                  </a-space>
+                  <a-dropdown :trigger="['click']" class="share-mobile">
+                    <a-button size="small">
+                      <template #icon><CopyOutlined /></template>
+                      {{ t('copy') }}
+                    </a-button>
+                    <template #overlay>
+                      <a-menu @click="({ key }) => {
+                        const h = dbInbound.address;
+                        const port = dbInbound.port;
+                        if (key === 'telegram') {
+                          copyText(`https://t.me/socks?server=${encodeURIComponent(h)}&port=${port}&user=${encodeURIComponent(account.user)}&pass=${encodeURIComponent(account.pass)}`);
+                        } else {
+                          copyText(`${key}://${h}:${port}@${account.user}:${account.pass}`);
+                        }
+                      }">
+                        <a-menu-item key="socks5">SOCKS5</a-menu-item>
+                        <a-menu-item key="http">HTTP</a-menu-item>
+                        <a-menu-item key="telegram">Telegram</a-menu-item>
+                      </a-menu>
+                    </template>
+                  </a-dropdown>
+                </dd>
+              </div>
+            </template>
+
+            <template v-if="inbound.settings.auth === 'noauth'">
+              <div class="info-row">
+                <dt>{{ t('copy') }}</dt>
+                <dd>
+                  <a-space :size="4" wrap class="share-buttons share-desktop">
+                    <a-tooltip :title="`socks5://${dbInbound.address}:${dbInbound.port}`">
+                      <a-button size="small"
+                        @click="copyText(`socks5://${dbInbound.address}:${dbInbound.port}`)">
+                        SOCKS5
+                      </a-button>
+                    </a-tooltip>
+                    <a-tooltip :title="`http://${dbInbound.address}:${dbInbound.port}`">
+                      <a-button size="small"
+                        @click="copyText(`http://${dbInbound.address}:${dbInbound.port}`)">
+                        HTTP
+                      </a-button>
+                    </a-tooltip>
+                    <a-tooltip title="https://t.me/socks?server=...&port=...">
+                      <a-button size="small"
+                        @click="copyText(`https://t.me/socks?server=${encodeURIComponent(dbInbound.address)}&port=${dbInbound.port}`)">
+                        Telegram
+                      </a-button>
+                    </a-tooltip>
+                  </a-space>
+                  <a-dropdown :trigger="['click']" class="share-mobile">
+                    <a-button size="small">
+                      <template #icon><CopyOutlined /></template>
+                      {{ t('copy') }}
+                    </a-button>
+                    <template #overlay>
+                      <a-menu @click="({ key }) => {
+                        const h = dbInbound.address;
+                        const port = dbInbound.port;
+                        if (key === 'telegram') {
+                          copyText(`https://t.me/socks?server=${encodeURIComponent(h)}&port=${port}`);
+                        } else {
+                          copyText(`${key}://${h}:${port}`);
+                        }
+                      }">
+                        <a-menu-item key="socks5">SOCKS5</a-menu-item>
+                        <a-menu-item key="http">HTTP</a-menu-item>
+                        <a-menu-item key="telegram">Telegram</a-menu-item>
+                      </a-menu>
+                    </template>
+                  </a-dropdown>
                 </dd>
               </div>
             </template>
@@ -897,6 +967,7 @@ const showSubscriptionTab = computed(
   white-space: normal;
   word-break: break-all;
   display: inline-block;
+  margin-right: 0;
 }
 
 .value-block {
@@ -927,6 +998,27 @@ const showSubscriptionTab = computed(
   flex-shrink: 0;
 }
 
+.share-buttons,
+.share-mobile {
+  margin-inline-start: 4px;
+  padding-inline-start: 8px;
+  border-inline-start: 1px solid rgba(128, 128, 128, 0.25);
+}
+
+.share-mobile {
+  display: none;
+}
+
+@media (max-width: 600px) {
+  .share-desktop {
+    display: none !important;
+  }
+  .share-mobile {
+    display: inline-flex;
+    align-items: center;
+  }
+}
+
 .security-line {
   display: flex;
   align-items: center;

+ 7 - 0
frontend/src/pages/login/LoginPage.vue

@@ -469,6 +469,13 @@ function cycleTheme() {
   font-weight: 500;
 }
 
+.login-form :deep(input.ant-input:-webkit-autofill) {
+  -webkit-text-fill-color: var(--color-text) !important;
+  -webkit-box-shadow: 0 0 0 1000px var(--bg-card) inset !important;
+  box-shadow: 0 0 0 1000px var(--bg-card) inset !important;
+  transition: background-color 9999s ease-in-out 0s, color 9999s ease-in-out 0s;
+}
+
 .submit-row {
   margin-bottom: 0;
 }

+ 1 - 1
frontend/src/pages/sub/SubPage.vue

@@ -125,7 +125,7 @@ const shadowrocketUrl = computed(() => {
   if (!subUrl) return '';
   const separator = subUrl.includes('?') ? '&' : '?';
   const rawUrl = subUrl + separator + 'flag=shadowrocket';
-  const base64Url = encodeURIComponent(btoa(rawUrl));
+  const base64Url = btoa(rawUrl).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
   const remark = encodeURIComponent(subTitle || sId || 'Subscription');
   return `shadowrocket://add/sub/${base64Url}?remark=${remark}`;
 });

+ 47 - 0
frontend/src/pages/xray/OutboundFormModal.vue

@@ -328,6 +328,47 @@ function regenerateWgKeys() {
                 </a-select>
               </a-form-item>
             </template>
+
+            <a-form-item label="Final Rules">
+              <a-button size="small" type="primary" @click="outbound.settings.addFinalRule('allow')">
+                <template #icon>
+                  <PlusOutlined />
+                </template>
+              </a-button>
+              <span class="ml-8" style="opacity: 0.6;">
+                Override Xray's default private-IP block (needed for LAN access through proxy)
+              </span>
+            </a-form-item>
+            <template v-for="(rule, index) in outbound.settings.finalRules || []" :key="`fr-${index}`">
+              <a-form-item :wrapper-col="{ md: { span: 14, offset: 8 } }" :colon="false">
+                <div class="item-heading">
+                  <span>Rule {{ index + 1 }}</span>
+                  <DeleteOutlined class="danger-icon" @click="outbound.settings.delFinalRule(index)" />
+                </div>
+              </a-form-item>
+              <a-form-item label="Action">
+                <a-select v-model:value="rule.action">
+                  <a-select-option v-for="x in ['allow', 'block']" :key="x" :value="x">{{ x }}</a-select-option>
+                </a-select>
+              </a-form-item>
+              <a-form-item label="Network">
+                <a-select v-model:value="rule.network" allow-clear placeholder="(any)">
+                  <a-select-option value="tcp">tcp</a-select-option>
+                  <a-select-option value="udp">udp</a-select-option>
+                  <a-select-option value="tcp,udp">tcp,udp</a-select-option>
+                </a-select>
+              </a-form-item>
+              <a-form-item label="Port">
+                <a-input v-model:value="rule.port" placeholder="e.g. 80,443 or 1000-2000" />
+              </a-form-item>
+              <a-form-item label="IP / CIDR / geoip">
+                <a-select v-model:value="rule.ip" mode="tags" :token-separators="[',', ' ']"
+                  placeholder="e.g. 10.0.0.0/8, geoip:private, ext:cn.dat:cn" />
+              </a-form-item>
+              <a-form-item v-if="rule.action === 'block'" label="Block delay (ms)">
+                <a-input v-model:value="rule.blockDelay" placeholder="optional: 5000-10000" />
+              </a-form-item>
+            </template>
           </template>
 
           <!-- ============== Blackhole ============== -->
@@ -947,6 +988,12 @@ function regenerateWgKeys() {
               <a-form-item label="Penetrate">
                 <a-switch v-model:checked="outbound.stream.sockopt.penetrate" />
               </a-form-item>
+              <a-form-item label="Mark (fwmark)">
+                <a-input-number v-model:value="outbound.stream.sockopt.mark" :min="0" />
+              </a-form-item>
+              <a-form-item label="Interface">
+                <a-input v-model:value="outbound.stream.sockopt.interfaceName" />
+              </a-form-item>
             </template>
           </template>
 

+ 4 - 1
web/service/config.json

@@ -30,7 +30,10 @@
   "outbounds": [{
       "protocol": "freedom",
       "settings": {
-        "domainStrategy": "AsIs"
+        "domainStrategy": "AsIs",
+        "finalRules": [
+          { "action": "allow", "ip": ["geoip:private"] }
+        ]
       },
       "tag": "direct"
     },

+ 7 - 1
web/service/inbound.go

@@ -2845,7 +2845,7 @@ func (s *InboundService) MigrationRequirements() {
 
 	// Fix inbounds based problems
 	var inbounds []*model.Inbound
-	err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error
+	err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan", "shadowsocks", "hysteria", "hysteria2"}).Find(&inbounds).Error
 	if err != nil && err != gorm.ErrRecordNotFound {
 		return
 	}
@@ -2924,6 +2924,12 @@ func (s *InboundService) MigrationRequirements() {
 				}
 			}
 		}
+
+		// Heal clients table for installs where the one-shot seeder
+		// skipped clients due to a tgId-string unmarshal error.
+		if syncErr := s.clientService.SyncInbound(tx, inbounds[inbound_index].Id, modelClients); syncErr != nil {
+			logger.Warning("MigrationRequirements sync clients failed:", syncErr)
+		}
 	}
 	tx.Save(inbounds)
 

+ 2 - 1
web/web.go

@@ -154,7 +154,8 @@ func (s *Server) initRouter() (*gin.Engine, error) {
 
 	engine := gin.Default()
 	directHTTPS := s.isDirectHTTPSConfigured()
-	engine.Use(middleware.SecurityHeadersMiddleware(directHTTPS))
+	sendHSTS := directHTTPS && !config.IsSkipHSTS()
+	engine.Use(middleware.SecurityHeadersMiddleware(sendHSTS))
 
 	webDomain, err := s.settingService.GetWebDomain()
 	if err != nil {