diff --git a/.github/update.log b/.github/update.log
index bb118db97b..3f21cbf576 100644
--- a/.github/update.log
+++ b/.github/update.log
@@ -1188,3 +1188,4 @@ Update On Mon Nov 17 19:42:18 CET 2025
Update On Tue Nov 18 19:40:59 CET 2025
Update On Wed Nov 19 19:39:59 CET 2025
Update On Thu Nov 20 19:39:36 CET 2025
+Update On Fri Nov 21 19:35:09 CET 2025
diff --git a/clash-nyanpasu/backend/Cargo.lock b/clash-nyanpasu/backend/Cargo.lock
index bfb4bbbd41..b89e781041 100644
--- a/clash-nyanpasu/backend/Cargo.lock
+++ b/clash-nyanpasu/backend/Cargo.lock
@@ -710,9 +710,9 @@ dependencies = [
[[package]]
name = "axum"
-version = "0.8.6"
+version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871"
+checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425"
dependencies = [
"axum-core",
"base64 0.22.1",
@@ -1688,7 +1688,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -2294,7 +2294,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.2",
- "windows-sys 0.61.2",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -6364,7 +6364,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
- "windows-sys 0.61.2",
+ "windows-sys 0.45.0",
]
[[package]]
@@ -7880,7 +7880,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -9549,7 +9549,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix 1.0.8",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -11248,7 +11248,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys 0.48.0",
]
[[package]]
diff --git a/clash-nyanpasu/frontend/nyanpasu/package.json b/clash-nyanpasu/frontend/nyanpasu/package.json
index 95afe5ebf1..fc68469f8d 100644
--- a/clash-nyanpasu/frontend/nyanpasu/package.json
+++ b/clash-nyanpasu/frontend/nyanpasu/package.json
@@ -80,14 +80,14 @@
"clsx": "2.1.1",
"core-js": "3.46.0",
"filesize": "11.0.13",
- "meta-json-schema": "1.19.14",
+ "meta-json-schema": "1.19.16",
"monaco-yaml": "5.4.0",
"nanoid": "5.1.6",
"sass-embedded": "1.93.3",
"shiki": "2.5.0",
"unplugin-auto-import": "20.2.0",
"unplugin-icons": "22.5.0",
- "validator": "13.15.20",
+ "validator": "13.15.23",
"vite": "7.2.2",
"vite-plugin-html": "3.2.2",
"vite-plugin-sass-dts": "1.3.34",
diff --git a/clash-nyanpasu/package.json b/clash-nyanpasu/package.json
index 4a3dfb599a..534f450627 100644
--- a/clash-nyanpasu/package.json
+++ b/clash-nyanpasu/package.json
@@ -69,7 +69,7 @@
"@types/node": "24.10.0",
"@typescript-eslint/eslint-plugin": "8.46.3",
"@typescript-eslint/parser": "8.46.3",
- "autoprefixer": "10.4.21",
+ "autoprefixer": "10.4.22",
"conventional-changelog-conventionalcommits": "9.1.0",
"cross-env": "10.1.0",
"dedent": "1.7.0",
diff --git a/clash-nyanpasu/pnpm-lock.yaml b/clash-nyanpasu/pnpm-lock.yaml
index ac14f03f7c..552b8f4751 100644
--- a/clash-nyanpasu/pnpm-lock.yaml
+++ b/clash-nyanpasu/pnpm-lock.yaml
@@ -56,8 +56,8 @@ importers:
specifier: 8.46.3
version: 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
autoprefixer:
- specifier: 10.4.21
- version: 10.4.21(postcss@8.5.6)
+ specifier: 10.4.22
+ version: 10.4.22(postcss@8.5.6)
conventional-changelog-conventionalcommits:
specifier: 9.1.0
version: 9.1.0
@@ -418,8 +418,8 @@ importers:
specifier: 11.0.13
version: 11.0.13
meta-json-schema:
- specifier: 1.19.14
- version: 1.19.14
+ specifier: 1.19.16
+ version: 1.19.16
monaco-yaml:
specifier: 5.4.0
version: 5.4.0(monaco-editor@0.54.0)
@@ -439,8 +439,8 @@ importers:
specifier: 22.5.0
version: 22.5.0(@svgr/core@8.1.0(typescript@5.9.3))
validator:
- specifier: 13.15.20
- version: 13.15.20
+ specifier: 13.15.23
+ version: 13.15.23
vite:
specifier: 7.2.2
version: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass-embedded@1.93.3)(sass@1.93.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1)
@@ -4040,8 +4040,8 @@ packages:
async@3.2.6:
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
- autoprefixer@10.4.21:
- resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==}
+ autoprefixer@10.4.22:
+ resolution: {integrity: sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==}
engines: {node: ^10 || ^12 || >=14}
hasBin: true
peerDependencies:
@@ -4085,6 +4085,10 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+ baseline-browser-mapping@2.8.29:
+ resolution: {integrity: sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==}
+ hasBin: true
+
before-after-hook@2.2.3:
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
@@ -4130,13 +4134,13 @@ packages:
peerDependencies:
browserslist: '*'
- browserslist@4.24.4:
- resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==}
+ browserslist@4.25.1:
+ resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
- browserslist@4.25.1:
- resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==}
+ browserslist@4.28.0:
+ resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
@@ -4214,12 +4218,12 @@ packages:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
- caniuse-lite@1.0.30001702:
- resolution: {integrity: sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==}
-
caniuse-lite@1.0.30001726:
resolution: {integrity: sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==}
+ caniuse-lite@1.0.30001756:
+ resolution: {integrity: sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==}
+
capture-stack-trace@1.0.2:
resolution: {integrity: sha512-X/WM2UQs6VMHUtjUDnZTRI+i1crWteJySFzr9UpGoQa4WQffXVTTXuekjl7TjZRlcF2XfjgITT0HxZ9RnxeT0w==}
engines: {node: '>=0.10.0'}
@@ -4882,8 +4886,8 @@ packages:
electron-to-chromium@1.5.175:
resolution: {integrity: sha512-Nqpef9mOVo7pZfl9NIUhj7tgtRTsMzCzRTJDP1ccim4Wb4YHOz3Le87uxeZq68OCNwau2iQ/X7UwdAZ3ReOkmg==}
- electron-to-chromium@1.5.92:
- resolution: {integrity: sha512-BeHgmNobs05N1HMmMZ7YIuHfYBGlq/UmvlsTgg+fsbFs9xVMj+xJHFg19GN04+9Q+r8Xnh9LXqaYIyEWElnNgQ==}
+ electron-to-chromium@1.5.258:
+ resolution: {integrity: sha512-rHUggNV5jKQ0sSdWwlaRDkFc3/rRJIVnOSe9yR4zrR07m3ZxhP4N27Hlg8VeJGGYgFTxK5NqDmWI4DSH72vIJg==}
electron@23.3.13:
resolution: {integrity: sha512-BaXtHEb+KYKLouUXlUVDa/lj9pj4F5kiE0kwFdJV84Y2EU7euIDgPthfKtchhr5MVHmjtavRMIV/zAwEiSQ9rQ==}
@@ -5379,8 +5383,8 @@ packages:
engines: {node: '>=18.3.0'}
hasBin: true
- fraction.js@4.3.7:
- resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
+ fraction.js@5.3.4:
+ resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
framer-motion@12.23.24:
resolution: {integrity: sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==}
@@ -6508,8 +6512,8 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
- meta-json-schema@1.19.14:
- resolution: {integrity: sha512-A+NSHAfXWn2T225dawVuLXVXrSWhxjRNiG+nS+Cet1Zovslrq2lMqvkIrXhdaK6Gv+VYrEV8rAkYcqAz2pxKMw==}
+ meta-json-schema@1.19.16:
+ resolution: {integrity: sha512-Py3XR3VRXs3tAMg3sy7fmex8IU4p4FTxVbF86WTtssWpFcSNbBUjk0QjpdhGrh+9qPMSwCJY1drXnvgDq9XQ7Q==}
engines: {node: '>=18', pnpm: '>=9'}
micromark-core-commonmark@2.0.1:
@@ -6752,6 +6756,9 @@ packages:
node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
+ node-releases@2.0.27:
+ resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
+
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
@@ -8431,14 +8438,14 @@ packages:
resolution: {integrity: sha512-N0XH6lqDtFH84JxptQoZYmloF4nzrQqqrAymNj+/gW60AO2AZgOcf4O/nUXJcYfyQkqvMo9lSupBZmmgvuVXlw==}
engines: {node: '>=4'}
- update-browserslist-db@1.1.1:
- resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==}
+ update-browserslist-db@1.1.3:
+ resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
- update-browserslist-db@1.1.3:
- resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
+ update-browserslist-db@1.1.4:
+ resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
@@ -8477,8 +8484,8 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
- validator@13.15.20:
- resolution: {integrity: sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==}
+ validator@13.15.23:
+ resolution: {integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==}
engines: {node: '>= 0.10'}
varint@6.0.0:
@@ -12624,11 +12631,11 @@ snapshots:
async@3.2.6: {}
- autoprefixer@10.4.21(postcss@8.5.6):
+ autoprefixer@10.4.22(postcss@8.5.6):
dependencies:
- browserslist: 4.24.4
- caniuse-lite: 1.0.30001702
- fraction.js: 4.3.7
+ browserslist: 4.28.0
+ caniuse-lite: 1.0.30001756
+ fraction.js: 5.3.4
normalize-range: 0.1.2
picocolors: 1.1.1
postcss: 8.5.6
@@ -12685,6 +12692,8 @@ snapshots:
base64-js@1.5.1: {}
+ baseline-browser-mapping@2.8.29: {}
+
before-after-hook@2.2.3: {}
before-after-hook@4.0.0: {}
@@ -12728,13 +12737,6 @@ snapshots:
browserslist: 4.25.1
meow: 13.2.0
- browserslist@4.24.4:
- dependencies:
- caniuse-lite: 1.0.30001702
- electron-to-chromium: 1.5.92
- node-releases: 2.0.19
- update-browserslist-db: 1.1.1(browserslist@4.24.4)
-
browserslist@4.25.1:
dependencies:
caniuse-lite: 1.0.30001726
@@ -12742,6 +12744,14 @@ snapshots:
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.1)
+ browserslist@4.28.0:
+ dependencies:
+ baseline-browser-mapping: 2.8.29
+ caniuse-lite: 1.0.30001756
+ electron-to-chromium: 1.5.258
+ node-releases: 2.0.27
+ update-browserslist-db: 1.1.4(browserslist@4.28.0)
+
buffer-builder@0.2.0: {}
buffer-crc32@0.2.13: {}
@@ -12824,10 +12834,10 @@ snapshots:
camelcase@6.3.0: {}
- caniuse-lite@1.0.30001702: {}
-
caniuse-lite@1.0.30001726: {}
+ caniuse-lite@1.0.30001756: {}
+
capture-stack-trace@1.0.2: {}
ccount@2.0.1: {}
@@ -13476,7 +13486,7 @@ snapshots:
electron-to-chromium@1.5.175: {}
- electron-to-chromium@1.5.92: {}
+ electron-to-chromium@1.5.258: {}
electron@23.3.13:
dependencies:
@@ -14243,7 +14253,7 @@ snapshots:
dependencies:
fd-package-json: 2.0.0
- fraction.js@4.3.7: {}
+ fraction.js@5.3.4: {}
framer-motion@12.23.24(@emotion/is-prop-valid@1.3.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
@@ -15418,7 +15428,7 @@ snapshots:
merge2@1.4.1: {}
- meta-json-schema@1.19.14: {}
+ meta-json-schema@1.19.16: {}
micromark-core-commonmark@2.0.1:
dependencies:
@@ -15743,6 +15753,8 @@ snapshots:
node-releases@2.0.19: {}
+ node-releases@2.0.27: {}
+
normalize-path@3.0.0: {}
normalize-range@0.1.2: {}
@@ -17608,18 +17620,18 @@ snapshots:
unzip-response@2.0.1: {}
- update-browserslist-db@1.1.1(browserslist@4.24.4):
- dependencies:
- browserslist: 4.24.4
- escalade: 3.2.0
- picocolors: 1.1.1
-
update-browserslist-db@1.1.3(browserslist@4.25.1):
dependencies:
browserslist: 4.25.1
escalade: 3.2.0
picocolors: 1.1.1
+ update-browserslist-db@1.1.4(browserslist@4.28.0):
+ dependencies:
+ browserslist: 4.28.0
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
update-notifier@2.5.0:
dependencies:
boxen: 1.3.0
@@ -17661,7 +17673,7 @@ snapshots:
util-deprecate@1.0.2: {}
- validator@13.15.20: {}
+ validator@13.15.23: {}
varint@6.0.0: {}
diff --git a/geoip/.github/workflows/build.yml b/geoip/.github/workflows/build.yml
index 6b084d5517..2c83554362 100644
--- a/geoip/.github/workflows/build.yml
+++ b/geoip/.github/workflows/build.yml
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout codebase
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
fetch-depth: 0
diff --git a/mieru/pkg/common/debug.go b/mieru/pkg/common/debug.go
index 8d98ead17d..810f951056 100644
--- a/mieru/pkg/common/debug.go
+++ b/mieru/pkg/common/debug.go
@@ -1,3 +1,18 @@
+// Copyright (C) 2025 mieru authors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
package common
import (
diff --git a/mieru/pkg/protocol/underlay_base.go b/mieru/pkg/protocol/underlay_base.go
index 3a0cf8a723..02b03840f2 100644
--- a/mieru/pkg/protocol/underlay_base.go
+++ b/mieru/pkg/protocol/underlay_base.go
@@ -36,8 +36,8 @@ const (
sessionCleanInterval = 5 * time.Second
// Buffer received network packets before they are dropped by OS kernel.
- packetChanCapacityClient = segmentTreeCapacity
- packetChanCapacityServer = segmentTreeCapacity
+ packetChanCapacityClient = 1024
+ packetChanCapacityServer = 1024
)
// baseUnderlay contains a partial implementation of underlay.
diff --git a/mieru/tools/scan_license.sh b/mieru/tools/scan_license.sh
new file mode 100755
index 0000000000..2a93ea0613
--- /dev/null
+++ b/mieru/tools/scan_license.sh
@@ -0,0 +1,105 @@
+#!/bin/bash
+
+# Copyright (C) 2025 mieru authors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# This script scans all source code files and checks for GPL v3 license headers.
+
+# Change to project root directory.
+cd "$(dirname "$0")/.."
+
+# Colors for output.
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+NC='\033[0m' # No Color
+
+missing_files=()
+
+LICENSE_PATTERN="GNU General Public License"
+EXCLUDE_DIRS=(
+ ".git"
+ "vendor"
+)
+EXCEPTION_PATHS=(
+ "./deployments"
+ "./pkg/appctl/proto/google/protobuf"
+ "./pkg/congestion/rtt.go"
+ "./pkg/log"
+ "./pkg/deque"
+ "./pkg/socks5"
+)
+
+# Build find exclude arguments.
+EXCLUDE_ARGS=""
+for dir in "${EXCLUDE_DIRS[@]}"; do
+ EXCLUDE_ARGS="$EXCLUDE_ARGS -path ./$dir -prune -o"
+done
+
+# Check if a file should be excluded from license checking.
+is_exception() {
+ local file="$1"
+
+ for exception in "${EXCEPTION_PATHS[@]}"; do
+ # Check if file matches exception exactly or starts with exception path (for directories).
+ if [[ "$file" == "$exception" ]] || [[ "$file" == "$exception/"* ]]; then
+ return 0
+ fi
+ done
+ return 1
+}
+
+# Check if a file has the license header.
+check_license() {
+ local file="$1"
+
+ if is_exception "$file"; then
+ return 0
+ fi
+
+ if ! head -n 20 "$file" | grep -q "$LICENSE_PATTERN"; then
+ missing_files+=("$file")
+ return 1
+ fi
+ return 0
+}
+
+# Check all files matching a given pattern.
+check_files_by_pattern() {
+ local pattern="$1"
+ while IFS= read -r -d '' file; do
+ check_license "$file"
+ done < <(find . $EXCLUDE_ARGS -type f -name "$pattern" -print0)
+}
+
+echo "Scanning for license headers..."
+
+# Check different file types
+check_files_by_pattern "*.go"
+check_files_by_pattern "*.py"
+check_files_by_pattern "*.sh"
+check_files_by_pattern "*.proto"
+
+echo ""
+if [ ${#missing_files[@]} -eq 0 ]; then
+ echo -e "${GREEN}✓ All source files have license headers${NC}"
+ exit 0
+else
+ echo -e "${RED}✗ Found ${#missing_files[@]} file(s) missing license headers:${NC}"
+ echo ""
+ for file in "${missing_files[@]}"; do
+ echo " $file"
+ done
+ exit 1
+fi
diff --git a/openwrt-packages/luci-app-lucky/lucky/Makefile b/openwrt-packages/luci-app-lucky/lucky/Makefile
index 98c286d361..e76b090d04 100644
--- a/openwrt-packages/luci-app-lucky/lucky/Makefile
+++ b/openwrt-packages/luci-app-lucky/lucky/Makefile
@@ -9,7 +9,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=lucky
-PKG_VERSION:=2.19.5
+PKG_VERSION:=2.20.2
PKG_RELEASE:=1
PKGARCH:=all
diff --git a/openwrt-packages/luci-theme-argon/htdocs/luci-static/argon/css/cascade.css b/openwrt-packages/luci-theme-argon/htdocs/luci-static/argon/css/cascade.css
index cc387c0357..ddf5fbbd27 100644
--- a/openwrt-packages/luci-theme-argon/htdocs/luci-static/argon/css/cascade.css
+++ b/openwrt-packages/luci-theme-argon/htdocs/luci-static/argon/css/cascade.css
@@ -1,4322 +1 @@
-/**
- * Argon is a clean HTML5 theme for LuCI. It is based on luci-theme-material and Argon Template
- *
- * luci-theme-argon
- * Copyright 2025 Jerrykuku
- *
- * Have a bug? Please create an issue here on GitHub!
- * https://github.com/jerrykuku/luci-theme-argon/issues
- *
- * luci-theme-material:
- * Copyright 2015 Lutty Yang
- * https://github.com/LuttyYang/luci-theme-material/
- *
- * Agron Theme
- * https://demos.creative-tim.com/argon-dashboard/index.html
- *
- * Login background
- * https://unsplash.com/
- * Font generate by Icomoon
- *
- * Licensed to the public under the Apache License 2.0
- */
-:root {
- --primary: #5e72e4;
- --dark-primary: #483d8b;
- --header-color: #fff;
- --menu-bg-color: #ffffff;
- --menu-color: #5f6368;
- --main-menu-color: #202124;
- --red: #f5365c;
- --orange: #fb6340;
- --yellow: #ffd600;
- --green: #2dce89;
- --teal: #11cdef;
- --cyan: #2bffc6;
- --gray: #8898aa;
- --gray-dark: #32325d;
- --light: #ced4da;
- --lighter: #e9ecef;
- --success: #2dce89;
- --info: #11cdef;
- --warning: #fb6340;
- --danger: #f5365c;
- --light: #adb5bd;
- --dark: #212529;
- --default: #172b4d;
- --white: #fff;
- --darker: black;
- --background-color: #f4f5f7;
- --login-form-bg-color: rgba(244, 245, 247, 0.8);
- --blur-radius: 10px;
- --blur-opacity: 0.5;
- --blur-radius-dark: 10px;
- --blur-opacity-dark: 0.5;
- --font-family-sans-serif: "Google Sans", "Microsoft Yahei", "WenQuanYi Micro Hei", "sans-serif", "Helvetica Neue", "Helvetica", "Hiragino Sans GB";
- --dropdown-arrow-icon: url("data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik01LjI5MyA5LjI5M2ExIDEgMCAwIDEgMS40MTQgMEwxMiAxNC41ODZsNS4yOTMtNS4yOTNhMSAxIDAgMSAxIDEuNDE0IDEuNDE0bC02IDZhMSAxIDAgMCAxLTEuNDE0IDBsLTYtNmExIDEgMCAwIDEgMC0xLjQxNCIvPjwvc3ZnPg==");
-}
-/*
- * Include base and custom css
- */
-@font-face {
- font-family: 'Google Sans';
- src: local('Google Sans'), local('GoogleSans-Regular'), url('../fonts/GoogleSans-Regular.woff2') format('woff2'), url('../fonts/GoogleSans-Regular.woff') format('woff');
- font-weight: normal;
- font-style: normal;
- font-display: swap;
-}
-/* Logo Font */
-@font-face {
- font-family: 'TypoGraphica';
- src: local('TypoGraphica'), url('../fonts/TypoGraphica.woff2') format('woff2'), url('../fonts/TypoGraphica.woff') format('woff');
- font-weight: normal;
- font-style: normal;
- font-display: swap;
-}
-/* Icon Font */
-@font-face {
- font-family: 'argon';
- src: url('../fonts/argon.woff2') format('woff2'), url('../fonts/argon.woff') format('woff');
- font-weight: normal;
- font-style: normal;
- font-display: swap;
-}
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-html {
- -webkit-text-size-adjust: 100%;
- -ms-text-size-adjust: 100%;
-}
-html,
-body {
- margin: 0;
- padding: 0;
- height: 100%;
- font-family: var(--font-family-sans-serif);
-}
-body {
- background-color: var(--background-color);
- color: var(--gray-dark);
- -webkit-tap-highlight-color: transparent;
- font-size: 0.875rem;
-}
-main {
- display: block;
-}
-.h1,
-.h2,
-.h3,
-.h4,
-.h5,
-.h6,
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
- font-family: inherit;
- font-weight: normal;
- line-height: 1.1 !important;
- color: inherit;
-}
-h1 {
- font-size: 2rem;
- margin: 0.67em 0;
- padding-bottom: 0.5rem;
- border-bottom: thin solid var(--lighter);
-}
-h2 {
- padding: 1rem 1.25rem;
- font-size: 1.25rem;
- font-weight: bold;
- color: var(--gray-dark);
- border-radius: 0.25rem;
- background: var(--white);
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03);
-}
-h3 {
- margin: 0;
- padding: 0.8755rem 1.25rem;
- font-size: 1.1rem;
- font-weight: bold;
- line-height: 1;
- color: var(--gray-dark);
- border-radius: 0.25rem;
- background: var(--white);
- display: block;
- width: 100%;
-}
-h4 {
- margin: 0;
- padding: 0.75rem 1.25rem;
- font-size: 0.875rem;
- font-weight: bold;
- color: var(--gray-dark-400);
-}
-h4 em {
- padding: 0 0.5rem;
-}
-h5 {
- margin: 2rem 0 0 0;
- padding-bottom: 0.5rem;
- font-size: 1rem;
-}
-a {
- background-color: transparent;
-}
-abbr {
- cursor: help;
- text-decoration: underline;
- color: var(--primary);
-}
-abbr[title] {
- border-bottom: none;
- text-decoration: underline;
- -webkit-text-decoration: underline dotted;
- text-decoration: underline dotted;
-}
-b,
-strong {
- font-weight: bolder;
-}
-code,
-kbd,
-samp {
- font-family: inherit;
- font-size: inherit;
- padding: 1px 3px;
- color: var(--dark);
- border-radius: 0.5rem;
- background: var(--lighter);
-}
-small {
- font-size: 90%;
- line-height: 1.42857143;
- white-space: normal;
-}
-sub,
-sup {
- font-size: 75%;
- line-height: 0;
- position: relative;
- vertical-align: baseline;
-}
-sub {
- bottom: -0.25em;
-}
-sup {
- top: -0.5em;
-}
-hr {
- box-sizing: content-box;
- height: 0;
- margin: 1rem 0;
- overflow: visible;
- opacity: 0.1;
- border-color: var(--lighter);
-}
-pre {
- font-family: monospace, monospace;
- font-size: 1em;
- overflow: auto;
-}
-img {
- border-style: none;
-}
-button,
-input,
-optgroup,
-select,
-textarea {
- font-family: inherit;
- font-size: 100%;
- line-height: 1.15;
- margin: 0;
-}
-button,
-input {
- overflow: visible;
-}
-button,
-select {
- text-transform: none;
-}
-[type=button],
-[type=reset],
-[type=submit],
-button {
- -webkit-appearance: button;
- appearance: button;
-}
-[type=button]::-moz-focus-inner,
-[type=reset]::-moz-focus-inner,
-[type=submit]::-moz-focus-inner,
-button::-moz-focus-inner {
- border-style: none;
- padding: 0;
-}
-[type=button]:-moz-focusring,
-[type=reset]:-moz-focusring,
-[type=submit]:-moz-focusring,
-button:-moz-focusring {
- outline: 1px dotted ButtonText;
-}
-[type=checkbox],
-[type=radio] {
- box-sizing: border-box;
- padding: 0;
-}
-[type=number]::-webkit-inner-spin-button,
-[type=number]::-webkit-outer-spin-button {
- height: auto;
-}
-[type=search] {
- -webkit-appearance: textfield;
- appearance: textfield;
- outline-offset: -2px;
-}
-[type=search]::-webkit-search-decoration {
- -webkit-appearance: none;
- appearance: none;
-}
-::-webkit-file-upload-button {
- -webkit-appearance: button;
- appearance: button;
- font: inherit;
-}
-input[type="checkbox"] {
- appearance: none !important;
- -webkit-appearance: none !important;
- border: 1px solid var(--primary);
- width: 1rem !important;
- height: 1rem !important;
- padding: 0;
- cursor: pointer;
- transition: all 0.2s;
-}
-input[type="checkbox"]:checked {
- border: 1px solid var(--primary);
- background-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 8 8\'%3e%3cpath fill=\'%23fff\' d=\'M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z\'/%3e%3c/svg%3e') !important;
- background-color: var(--primary);
- background-size: 70%;
- background-repeat: no-repeat;
- background-position: center;
-}
-select {
- padding: 0.36rem 0.8rem;
- color: var(--gray-dark);
- border: thin solid var(--lighter);
- background-color: var(--white);
- background-image: none;
-}
-textarea {
- padding: 0.25rem;
- overflow: auto;
-}
-textarea:focus-visible {
- outline: none;
- border: 1px solid var(--primary);
-}
-fieldset {
- padding: 0.35em 0.75em 0.625em;
-}
-legend {
- box-sizing: border-box;
- color: inherit;
- display: table;
- max-width: 100%;
- padding: 0;
- white-space: normal;
-}
-details {
- display: block;
-}
-summary {
- display: list-item;
-}
-template {
- display: none;
-}
-progress {
- vertical-align: baseline;
-}
-ul {
- line-height: normal;
-}
-li {
- list-style-type: none;
-}
-[disabled="disabled"] {
- pointer-events: none;
-}
-::selection {
- background-color: var(--primary);
- color: var(--white);
-}
-::placeholder {
- color: var(--lighter);
-}
-a:link,
-a:visited,
-a:active {
- color: var(--primary);
- text-decoration: none;
-}
-a:hover {
- text-decoration: underline;
-}
-.pull-right {
- float: right;
-}
-.pull-left {
- float: left;
-}
-.nowrap:not(.td) {
- white-space: nowrap;
-}
-div[style="width:100%;height:300px;border:1px solid #000;background:#fff"] {
- border: 0 !important;
-}
-/**
-Pure v3.0.0
-Copyright 2013 Yahoo!
-Licensed under the BSD License.
-https://github.com/pure-css/pure/blob/master/LICENSE
-*/
-/**
-normalize.css v | MIT License | https://necolas.github.io/normalize.css/
-Copyright (c) Nicolas Gallagher and Jonathan Neal
-*/
-/** normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
-.pure-g {
- display: flex;
- flex-flow: row wrap;
- align-content: flex-start;
-}
-.pure-u {
- display: inline-block;
- vertical-align: top;
-}
-.pure-u-1,
-.pure-u-1-1,
-.pure-u-1-12,
-.pure-u-1-2,
-.pure-u-1-24,
-.pure-u-1-3,
-.pure-u-1-4,
-.pure-u-1-5,
-.pure-u-1-6,
-.pure-u-1-8,
-.pure-u-10-24,
-.pure-u-11-12,
-.pure-u-11-24,
-.pure-u-12-24,
-.pure-u-13-24,
-.pure-u-14-24,
-.pure-u-15-24,
-.pure-u-16-24,
-.pure-u-17-24,
-.pure-u-18-24,
-.pure-u-19-24,
-.pure-u-2-24,
-.pure-u-2-3,
-.pure-u-2-5,
-.pure-u-20-24,
-.pure-u-21-24,
-.pure-u-22-24,
-.pure-u-23-24,
-.pure-u-24-24,
-.pure-u-3-24,
-.pure-u-3-4,
-.pure-u-3-5,
-.pure-u-3-8,
-.pure-u-4-24,
-.pure-u-4-5,
-.pure-u-5-12,
-.pure-u-5-24,
-.pure-u-5-5,
-.pure-u-5-6,
-.pure-u-5-8,
-.pure-u-6-24,
-.pure-u-7-12,
-.pure-u-7-24,
-.pure-u-7-8,
-.pure-u-8-24,
-.pure-u-9-24 {
- display: inline-block;
- letter-spacing: normal;
- word-spacing: normal;
- vertical-align: top;
- text-rendering: auto;
-}
-.pure-u-1-24 {
- width: 4.1667%;
-}
-.pure-u-1-12,
-.pure-u-2-24 {
- width: 8.3333%;
-}
-.pure-u-1-8,
-.pure-u-3-24 {
- width: 12.5%;
-}
-.pure-u-1-6,
-.pure-u-4-24 {
- width: 16.6667%;
-}
-.pure-u-1-5 {
- width: 20%;
-}
-.pure-u-5-24 {
- width: 20.8333%;
-}
-.pure-u-1-4,
-.pure-u-6-24 {
- width: 25%;
-}
-.pure-u-7-24 {
- width: 29.1667%;
-}
-.pure-u-1-3,
-.pure-u-8-24 {
- width: 33.3333%;
-}
-.pure-u-3-8,
-.pure-u-9-24 {
- width: 37.5%;
-}
-.pure-u-2-5 {
- width: 40%;
-}
-.pure-u-10-24,
-.pure-u-5-12 {
- width: 41.6667%;
-}
-.pure-u-11-24 {
- width: 45.8333%;
-}
-.pure-u-1-2,
-.pure-u-12-24 {
- width: 50%;
-}
-.pure-u-13-24 {
- width: 54.1667%;
-}
-.pure-u-14-24,
-.pure-u-7-12 {
- width: 58.3333%;
-}
-.pure-u-3-5 {
- width: 60%;
-}
-.pure-u-15-24,
-.pure-u-5-8 {
- width: 62.5%;
-}
-.pure-u-16-24,
-.pure-u-2-3 {
- width: 66.6667%;
-}
-.pure-u-17-24 {
- width: 70.8333%;
-}
-.pure-u-18-24,
-.pure-u-3-4 {
- width: 75%;
-}
-.pure-u-19-24 {
- width: 79.1667%;
-}
-.pure-u-4-5 {
- width: 80%;
-}
-.pure-u-20-24,
-.pure-u-5-6 {
- width: 83.3333%;
-}
-.pure-u-21-24,
-.pure-u-7-8 {
- width: 87.5%;
-}
-.pure-u-11-12,
-.pure-u-22-24 {
- width: 91.6667%;
-}
-.pure-u-23-24 {
- width: 95.8333%;
-}
-.pure-u-1,
-.pure-u-1-1,
-.pure-u-24-24,
-.pure-u-5-5 {
- width: 100%;
-}
-.col-1 {
- flex: 1 1 30px !important;
-}
-.col-2 {
- flex: 2 2 60px !important;
-}
-.col-3 {
- flex: 3 3 90px !important;
-}
-.col-4 {
- flex: 4 4 120px !important;
-}
-.col-5 {
- flex: 5 5 150px !important;
-}
-.col-6 {
- flex: 6 6 180px !important;
-}
-.col-7 {
- flex: 7 7 210px !important;
-}
-.col-8 {
- flex: 8 8 240px !important;
-}
-.col-9 {
- flex: 9 9 270px !important;
-}
-.col-10 {
- flex: 10 10 300px !important;
-}
-.table {
- position: relative;
- display: table;
-}
-.tr {
- display: table-row;
-}
-.thead {
- display: table-header-group;
-}
-.tbody {
- display: table-row-group;
-}
-.tfoot {
- display: table-footer-group;
-}
-.td,
-.th {
- line-height: normal;
- display: table-cell;
- padding: 0.5em;
- text-align: center;
- vertical-align: middle;
-}
-.th {
- font-weight: bold;
- white-space: nowrap;
-}
-.tr.placeholder {
- height: 4em;
-}
-.tr.placeholder > .td {
- line-height: 3;
- position: absolute;
- right: 0;
- bottom: 0;
- left: 0;
- padding: 0.4rem 0 !important;
- text-align: center !important;
- background: inherit;
-}
-.td[width="33%"] {
- padding: 1.1em 1.5rem;
-}
-.table[width="33%"],
-.th[width="33%"],
-.td[width="33%"] {
- width: 33%;
-}
-.table[width="100%"],
-.th[width="100%"],
-.td[width="100%"] {
- width: 100%;
-}
-.btn,
-button,
-select,
-input,
-.cbi-dropdown {
- line-height: 1.5rem;
- height: 2.5rem;
- padding: 0.5rem 0.75rem;
- color: #8898aa;
- border: 1px solid #dee2e6;
- border-radius: 0.25rem;
- outline: 0;
- background-image: none;
- box-shadow: none;
- transition: box-shadow 0.15s ease;
-}
-select {
- padding-right: 1.5rem;
- background-image: url("data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjODg5OGFhIiBkPSJNNS4yOTMgOS4yOTNhMSAxIDAgMCAxIDEuNDE0IDBMMTIgMTQuNTg2bDUuMjkzLTUuMjkzYTEgMSAwIDEgMSAxLjQxNCAxLjQxNGwtNiA2YTEgMSAwIDAgMS0xLjQxNCAwbC02LTZhMSAxIDAgMCAxIDAtMS40MTQiLz48L3N2Zz4=");
- background-size: 1rem;
- background-position: right 0.5rem center;
- background-repeat: no-repeat;
- appearance: none;
-}
-select:not([multiple="multiple"]):focus,
-input:not(.cbi-button):focus,
-.cbi-dropdown:focus {
- border-color: var(--primary);
- box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1);
-}
-.cbi-dropdown,
-select[multiple="multiple"] {
- height: auto;
-}
-.login-page {
- display: flex;
- height: 100%;
- flex-direction: column;
-}
-.login-page .video {
- position: absolute;
- top: 0;
- left: 0;
- height: 100vh;
- width: 100vw;
- display: flex;
- align-items: center;
- justify-content: center;
- background-color: var(--darker);
- overflow: hidden;
-}
-.login-page .video video {
- width: 100%;
- height: auto;
-}
-.login-page .volume-control {
- position: fixed;
- top: 1rem;
- right: 1rem;
- width: 1.5rem;
- height: 1.5rem;
- z-index: 5000;
- cursor: pointer;
- background: url(../img/volume_high.svg) no-repeat center / contain;
-}
-.login-page .volume-control.mute {
- background-image: url(../img/volume_off.svg);
-}
-.login-page .main-bg {
- position: absolute;
- top: 0;
- left: 0;
- height: 100vh;
- width: 100vw;
- background: url(../img/blank.png) no-repeat center / cover;
- transition: all 0.5s ease;
-}
-.login-page .login-container {
- z-index: 10;
- margin-left: 5%;
- display: flex;
- height: 100vh;
- width: 26rem;
- flex-direction: column;
- background-color: var(--white);
- backdrop-filter: blur(var(--blur-radius));
- background-color: rgba(244, 245, 247, var(--blur-opacity));
- box-shadow: rgba(0, 0, 0, 0.75) 0 0 35px -5px;
-}
-.login-page .login-container .login-form {
- position: absolute;
- top: 0;
- left: 0;
- height: 100vh;
- width: 100vw;
- display: flex;
- flex-direction: column;
- align-items: center;
- max-width: 26rem;
-}
-.login-page .login-container .login-form .brand {
- display: flex;
- align-items: center;
- justify-content: center;
- margin: 50px auto 100px 50px;
- color: var(--default);
- text-decoration: none;
-}
-.login-page .login-container .login-form .brand .icon {
- width: 50px;
- height: auto;
- margin-right: 25px;
-}
-.login-page .login-container .login-form .brand .brand-text {
- margin-right: 45px;
- font-size: 1.25rem;
- font-weight: 700;
- font-family: "TypoGraphica";
- word-break: break-word;
-}
-.login-page .login-container .login-form .brand:hover {
- text-decoration: none;
-}
-.login-page .login-container .login-form .form-login {
- width: 100%;
- padding: 20px 50px;
- box-sizing: border-box;
-}
-.login-page .login-container .login-form .form-login .errorbox {
- padding-bottom: 2rem;
- text-align: center;
- color: var(--warning);
-}
-.login-page .login-container .login-form .form-login .input-group {
- position: relative;
- margin-bottom: 1.25rem;
-}
-.login-page .login-container .login-form .form-login .input-group::before {
- font-family: 'argon' !important;
- font-style: normal;
- font-weight: normal;
- font-variant: normal;
- text-transform: none;
- line-height: 1;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- position: absolute;
- top: 10px;
- left: 10px;
- z-index: 100;
- font-size: 1.5rem;
- color: var(--default);
-}
-.login-page .login-container .login-form .form-login .input-group .border {
- position: absolute;
- bottom: 0;
- width: 100%;
- height: 1px;
- border-bottom: 1px solid var(--primary);
- transform: scaleX(0);
- transition: transform 0.3s ease;
-}
-.login-page .login-container .login-form .form-login .input-group input {
- display: block;
- width: 100%;
- margin: 0.825rem 0;
- padding: 0.5rem 0.75rem 0.5rem 3rem;
- font-size: 1rem;
- line-height: 1.5em;
- color: var(--default);
- background-color: transparent;
- background-clip: padding-box;
- border: 0;
- border-bottom: 1px solid var(--white);
- border-radius: 0;
- outline: none;
- box-sizing: border-box;
- box-shadow: 0 3px 2px rgba(233, 236, 239, 0.05);
- transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
-}
-.login-page .login-container .login-form .form-login .input-group input:focus + .border {
- transform: scaleX(1);
-}
-.login-page .login-container .login-form .form-login .input-group .cbi-input-password {
- position: relative;
- margin-bottom: 2rem;
-}
-.login-page .login-container .login-form .form-login .input-group.user-icon::before {
- content: "\e971";
-}
-.login-page .login-container .login-form .form-login .input-group.pass-icon::before {
- content: "\e910";
-}
-.login-page .login-container .login-form .cbi-button-apply {
- width: 100% !important;
- min-height: 50px;
- margin: 30px 0 100px;
- padding: 10px 0;
- font-size: 15px;
- font-weight: 600;
- color: var(--white);
- text-align: center;
- letter-spacing: 0.8rem;
- background-color: var(--primary) !important;
- border: none;
- border-radius: 6px;
- outline: none;
- cursor: pointer;
- box-shadow: rgba(0, 0, 0, 0.1) 0 0 50px 0;
- transition: all 0.3s ease !important;
-}
-.login-page .login-container .login-form .cbi-button-apply:hover,
-.login-page .login-container .login-form .cbi-button-apply:focus {
- opacity: 0.9;
-}
-.login-page .login-container footer {
- position: absolute;
- bottom: 0;
- z-index: 10;
- display: flex;
- justify-content: space-evenly;
- width: 100%;
- margin-top: auto;
- padding: 0 0 30px;
- color: var(--gray);
- text-align: center;
- line-height: 1.6rem;
- font-size: 0.75rem;
-}
-.login-page .login-container footer .ftc {
- position: absolute;
- bottom: 30px;
- width: 100%;
-}
-.login-page .login-container footer .luci-link {
- display: block;
-}
-.main {
- position: relative;
- width: 100%;
- height: 100%;
- overflow-y: auto;
- display: flex;
- flex-direction: row;
-}
-.main-left {
- width: 15rem;
- height: 100%;
- flex-shrink: 0;
- z-index: 100;
- overflow-x: auto;
- word-break: break-word;
- background-color: var(--menu-bg-color);
- box-shadow: rgba(0, 0, 0, 0.75) 0px 0px 15px -5px;
- transition: all 0.2s;
-}
-.main-left .sidenav-header {
- padding: 1.5rem 0.5rem;
- text-align: center;
-}
-.main-left .sidenav-header .brand {
- display: block;
- margin: 0 2rem;
- font-size: 1.8rem;
- font-family: "TypoGraphica";
- color: var(--primary);
- text-decoration: none;
- text-align: center;
- cursor: default;
-}
-.main-left .sidenav-header .brand .logo {
- max-width: 100%;
- height: auto;
-}
-.main-left .nav {
- margin-top: 0.5rem;
-}
-.main-left .nav > li > a:first-child {
- display: block;
- position: relative;
- margin: 0.1rem 0.5rem;
- padding: 0.675rem 0 0.675rem 2.5rem;
- font-size: 1rem;
- text-decoration: none;
- border-radius: 0.25rem;
- cursor: default;
- transition: all 0.2s;
-}
-.main-left .nav > li > a:first-child.active {
- color: #fff;
- background: var(--primary);
-}
-.main-left .nav > li > a:first-child.active::before {
- color: #fff !important;
-}
-.main-left .nav > li > a:first-child.active::after {
- transform: rotate(90deg);
- color: #fff !important;
-}
-.main-left .nav > li > a:first-child:hover {
- cursor: pointer;
- color: #fff;
- background: var(--primary);
-}
-.main-left .nav > li > a:first-child:hover::before {
- color: #fff !important;
-}
-.main-left .nav > li > a:first-child::before {
- position: absolute;
- left: 0.8rem;
- padding-top: 3px;
- font-family: 'argon' !important;
- font-style: normal;
- font-weight: normal;
- font-variant: normal;
- text-transform: none;
- line-height: 1;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- content: "\e915";
- color: var(--primary);
- transition: all 0.3s;
-}
-.main-left .nav > .slide > .menu::before {
- transition: transform 0.1s ease-in-out;
-}
-.main-left .nav > .slide > .menu.active::before {
- transition: transform 0.2s ease-in-out;
-}
-.main-left .nav li {
- padding: 0;
- cursor: pointer;
- user-select: none;
-}
-.main-left .nav li a {
- display: block;
- color: var(--menu-color);
-}
-.main-left .nav li.slide {
- padding: 0;
-}
-.main-left .nav li.slide ul {
- display: none;
- overflow: hidden;
-}
-.main-left .nav li.slide:hover {
- background: none;
-}
-.main-left .nav li.slide .slide-menu {
- margin: 0 0.5rem 0 2.5rem;
- padding: 0rem 0.5rem;
-}
-.main-left .nav li.slide .slide-menu.active {
- display: block;
-}
-.main-left .nav li.slide .slide-menu li {
- position: relative;
- margin: 0;
- border-radius: 0.25rem;
- background: none;
- list-style: none;
-}
-.main-left .nav li.slide .slide-menu li a {
- padding: 0.5rem 0rem;
- text-decoration: none;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-.main-left .nav li.slide .slide-menu li::after {
- position: absolute;
- left: 0;
- bottom: 0;
- width: 0;
- height: 2px;
- content: "";
- background-color: var(--primary);
- transition: all 0.2s;
-}
-.main-left .nav li.slide .slide-menu li:hover {
- background: none;
-}
-.main-left .nav li.slide .slide-menu li:hover::after {
- width: 100%;
-}
-.main-left .nav li.slide .slide-menu .active {
- background: none;
- color: var(--menu-color);
-}
-.main-left .nav li.slide .slide-menu .active a {
- color: var(--menu-color);
-}
-.main-left .nav li.slide .slide-menu .active::after {
- position: absolute;
- left: 0;
- bottom: 0;
- width: 100%;
- height: 2px;
- content: "";
- background-color: var(--primary);
- transition: all 0.2s;
-}
-.main-left .nav li.slide .slide-menu .active:hover {
- background: none;
-}
-.main-left .nav li.slide .slide-menu .active:hover::after {
- width: 100%;
-}
-.main-left .nav li .menu {
- display: block;
- position: relative;
- margin: 0.1rem 0.5rem;
- padding: 0.675rem 0 0.675rem 2.5rem;
- font-size: 1rem;
- text-decoration: none;
- border-radius: 0.25rem;
- cursor: default;
- transition: all 0.2s;
-}
-.main-left .nav li .menu.active {
- color: #fff;
- background: var(--primary);
-}
-.main-left .nav li .menu.active::before {
- color: #fff !important;
-}
-.main-left .nav li .menu.active::after {
- transform: rotate(90deg);
- color: #fff !important;
-}
-.main-left .nav li .menu:hover {
- cursor: pointer;
- color: #fff;
- background: var(--primary);
-}
-.main-left .nav li .menu:hover::before {
- color: #fff !important;
-}
-.main-left .nav li .menu::before {
- position: absolute;
- left: 0.8rem;
- padding-top: 3px;
- font-family: 'argon' !important;
- font-style: normal;
- font-weight: normal;
- font-variant: normal;
- text-transform: none;
- line-height: 1;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- content: "\e915";
- color: var(--primary);
- transition: all 0.3s;
-}
-.main-left .nav li .menu::after {
- position: absolute;
- right: 0.5rem;
- top: 0.8rem;
- font-family: 'argon' !important;
- font-style: normal;
- font-weight: normal;
- font-variant: normal;
- text-transform: none;
- line-height: 1;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- content: '\e90f';
- color: #ced4da;
- text-rendering: auto;
- transition: all 0.3s;
-}
-.main-left .nav li .menu[data-title="Status"]:before {
- content: "\e906";
- color: var(--primary);
-}
-.main-left .nav li .menu[data-title="System"]:before {
- content: "\e90a";
- color: #fb6340;
-}
-.main-left .nav li .menu[data-title="Services"]:before {
- content: "\e909";
- color: #11cdef;
-}
-.main-left .nav li .menu[data-title="NAS"]:before {
- content: "\e90c";
- color: #f3a4b5;
-}
-.main-left .nav li .menu[data-title="VPN"]:before {
- content: "\e90b";
- color: #8965e0;
-}
-.main-left .nav li .menu[data-title="Network"]:before {
- content: "\e908";
- color: #8965e0;
-}
-.main-left .nav li .menu[data-title="Bandwidth_Monitor"]:before {
- content: "\e90d";
- color: #2dce89;
-}
-.main-left .nav li .menu[data-title="Docker"]:before {
- content: "\e911";
- color: #6699ff;
-}
-.main-left .nav li .menu[data-title="Statistics"]:before {
- content: "\e913";
- color: #8965e0;
-}
-.main-left .nav li .menu[data-title="Control"]:before {
- content: "\e912";
- color: var(--primary);
-}
-.main-left .nav li .menu[data-title="Asterisk"]:before {
- content: "\e914";
- color: #fb6340;
-}
-.main-left .nav li a[data-title="Log_out"]::before,
-.main-left .nav li .food[data-title="Log_out"]::before {
- content: "\e907";
- color: #adb5bd;
-}
-.main-left[style*="overflow: hidden"] > .nav > .slide > .menu::before {
- display: none;
-}
-.main-left::-webkit-scrollbar {
- width: 5px;
- height: 1px;
-}
-.main-left::-webkit-scrollbar-thumb {
- background-color: #f6f9fc;
-}
-.main-left::-webkit-scrollbar-track {
- background-color: #fff;
-}
-.main-right {
- height: 100%;
- flex-grow: 1;
- overflow-x: hidden;
- overflow-y: auto;
- display: flex;
- flex-direction: column;
- transition: all 0.2s;
-}
-.main-right > #maincontent {
- position: relative;
- z-index: 50;
- flex: 1;
- display: flex;
- flex-direction: column;
-}
-.main-right > #maincontent > .container {
- margin: 0 1.25rem 1rem 1.25rem;
- flex-grow: 1;
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-.main-right > #maincontent .Dashboard {
- color: var(--gray-dark) !important;
-}
-.main-right > #maincontent .Dashboard h3 {
- color: var(--gray-dark);
-}
-.main-right > #maincontent .Dashboard p {
- margin-top: 3px;
- margin-bottom: 3px;
-}
-.main-right > #maincontent .Dashboard hr {
- border-top: 1px solid #000000;
-}
-.main-right > #maincontent .Dashboard .dashboard-bg {
- background-color: #fff;
-}
-.main-right > #maincontent .Dashboard .settings-info {
- padding-top: 1em;
- padding-bottom: 1em;
-}
-.main-right > #maincontent .Dashboard .settings-info p span:nth-child(2) {
- max-height: 18.5px;
- top: 4px;
-}
-.main-right > #maincontent .Dashboard .settings-info .label {
- font-size: 0.7rem;
- padding: 0.2rem 0.6rem;
-}
-header {
- position: relative;
- width: 100%;
- padding: 0;
- color: var(--header-color);
-}
-header.bg-primary {
- background-color: var(--primary) !important;
-}
-header::after {
- content: "";
- position: absolute;
- width: 100%;
- height: 2rem;
- background-color: var(--primary) !important;
-}
-header .fill {
- padding: 0.8rem 0;
- display: flex;
- border-bottom: 0px solid rgba(255, 255, 255, 0.08) !important;
-}
-header .fill .container {
- width: 100%;
- height: 2rem;
- padding: 0 1.25rem;
- display: flex;
- align-items: center;
-}
-header .fill .container .flex1 {
- flex: 1;
-}
-header .fill .container .flex1 .showSide {
- display: none;
- font-size: 1.4rem;
- color: #fff;
-}
-header .fill .container .flex1 .showSide:hover {
- text-decoration: none;
-}
-header .fill .container .flex1 .brand {
- display: none;
- padding-left: 1rem;
- font-size: 1.5rem;
- font-family: "TypoGraphica";
- color: #fff;
- text-decoration: none;
- cursor: default;
- vertical-align: text-bottom;
-}
-header .fill .container .pull-right {
- margin-top: 0rem;
- float: right;
- display: flex;
-}
-header .fill .status span {
- display: inline-block;
- margin: 0 0.25rem;
- padding: 0.3rem 0.8rem;
- font-size: 0.875rem;
- font-weight: bold;
- white-space: nowrap;
- text-decoration: none;
- text-transform: uppercase;
- text-shadow: none;
- border-radius: 4px;
- cursor: pointer;
- transition: all 0.3s;
- box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12);
-}
-header .fill .status span:last-child {
- margin-right: 0;
-}
-header .fill .status span[data-indicator="poll-status"] {
- color: #fff;
-}
-header .fill .status span[data-style="active"] {
- background-color: var(--green);
-}
-header .fill .status span[data-style="inactive"] {
- color: #ffffff !important;
- background-color: #32325d;
-}
-footer {
- padding: 1rem;
- font-size: 0.875rem;
- color: #aaa;
- text-align: right;
- white-space: nowrap;
- overflow: hidden;
-}
-footer > a {
- color: #aaa;
- text-decoration: none;
-}
-#view {
- border-radius: 0.25rem;
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-#view > .spinning {
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translateX(-50%) translateY(-50%);
- padding: 1rem;
- border-radius: 0.5rem;
- background: #ffffff;
- box-shadow: 0 0 1rem 0 rgba(136, 152, 170, 0.15);
-}
-#view > div:first-child {
- display: flex;
- flex-direction: column;
- gap: 0.875rem;
-}
-#xhr_poll_status {
- display: flex;
- margin-left: 0.5rem;
-}
-#xhr_poll_status * {
- color: #fff;
-}
-.danger {
- background-color: var(--danger) !important;
-}
-.warning {
- background-color: var(--warning) !important;
-}
-.success {
- background-color: var(--success) !important;
-}
-.notice {
- background-color: var(--info) !important;
- color: #fff;
-}
-.error {
- color: var(--danger);
-}
-.alert,
-.alert-message {
- position: fixed;
- width: 20rem;
- z-index: 9000;
- left: 50%;
- top: 50%;
- transform: translate(-50%, -50%);
- font-weight: bold;
- padding: 1rem 1.25rem;
- border: 0;
- border-radius: 0.25rem !important;
- background-color: #fff;
- box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12);
- text-shadow: none;
-}
-.alert.error,
-.alert-message.error {
- background-color: var(--warning);
-}
-.alert h4,
-.alert-message h4 {
- padding: 0.25rem 0rem;
- border-radius: 0.25rem;
-}
-.alert .btn,
-.alert-message .btn {
- height: auto;
-}
-.modal.alert-message {
- position: static;
- width: 90%;
- min-width: 270px;
- max-width: 600px;
- left: auto;
- top: auto;
- transform: none;
- margin: 5em auto;
-}
-.alert-message > h4 {
- font-size: 110%;
- font-weight: bold;
-}
-.alert-message > * {
- margin: 0.5rem 0;
-}
-.alert-message .btn {
- padding: 0.3rem 0.6rem;
-}
-.container .alert,
-.container .alert-message {
- margin-left: 0;
- margin-right: 0;
- margin-top: 0rem;
- position: relative;
- top: 0;
- transform: translate(-50%, 0);
- width: 100%;
-}
-#mwan3-service-status > .alert-message {
- left: 0.5rem;
- transform: none;
-}
-.lg {
- margin: 0;
- padding: 0 !important;
-}
-.cbi-section,
-.cbi-section-error,
-#iptables,
-.Firewall form,
-#cbi-network > .cbi-section-node,
-#cbi-wireless > .cbi-section-node,
-#cbi-wireless > #wifi_assoclist_table,
-[data-tab-title],
-[data-page^="admin-system-admin"]:not(.node-main-login) .cbi-map:not(#cbi-dropbear),
-[data-page="admin-system-opkg"] #maincontent > .container {
- font-family: inherit;
- font-weight: normal;
- font-style: normal;
- line-height: normal;
- min-width: inherit;
- padding: 0;
- border: 0;
- border-radius: 0.25rem;
- background-color: #fff;
- box-shadow: 0 0 1rem 0 rgba(136, 152, 170, 0.15);
-}
-.cbi-section:last-child,
-.cbi-section-error:last-child,
-#iptables:last-child,
-.Firewall form:last-child,
-#cbi-network > .cbi-section-node:last-child,
-#cbi-wireless > .cbi-section-node:last-child,
-#cbi-wireless > #wifi_assoclist_table:last-child,
-[data-tab-title]:last-child,
-[data-page^="admin-system-admin"]:not(.node-main-login) .cbi-map:not(#cbi-dropbear):last-child,
-[data-page="admin-system-opkg"] #maincontent > .container:last-child {
- margin: 0;
- border: 0;
-}
-.cbi-modal .cbi-section,
-.cbi-section .cbi-section {
- padding: 0;
- box-shadow: none;
-}
-.cbi-modal .cbi-tabmenu {
- margin-left: 0;
-}
-.cbi-map {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-.cbi-map > .cbi-tabmenu + div {
- margin-top: -0.4375rem;
-}
-.cbi-map-descr {
- font-size: small;
- line-height: 1.5;
- padding: 0 1.25rem;
-}
-.cbi-section > .cbi-section-descr {
- padding-top: 1rem !important;
- padding-bottom: 1rem !important;
-}
-.cbi-section > .cbi-section-descr:empty {
- display: none;
-}
-.cbi-section-descr:not(:empty) {
- font-size: small;
- line-height: 1.5;
- padding: 0rem 1rem;
-}
-.cbi-section-descr:empty {
- display: none !important;
-}
-.cbi-map-descr + fieldset {
- margin-top: 1rem;
-}
-.cbi-map-descr > abbr {
- cursor: help;
- text-decoration: underline;
-}
-.cbi-section > legend {
- display: none !important;
-}
-fieldset > fieldset,
-.cbi-section > .cbi-section {
- margin: 0;
- padding: 0;
- border: 0;
- box-shadow: none;
-}
-.cbi-section > h3:first-child,
-.panel-title {
- font-size: 1.1rem;
- line-height: 1;
- display: block;
- width: 100%;
- margin: 0;
- margin-bottom: 0;
- padding: 0.8755rem 1.25rem;
- color: #32325d;
- color: var(--gray-dark);
-}
-.cbi-section > h3:first-child,
-.cbi-section > h4:first-child,
-.cbi-section > p:first-child,
-[data-tab-title] > h3:first-child,
-[data-tab-title] > h4:first-child,
-[data-tab-title] > p:first-child {
- padding: 1rem 1.25rem;
-}
-.cbi-section p {
- padding: 1rem;
-}
-.cbi-tblsection {
- overflow-x: auto;
-}
-table {
- border-spacing: 0;
- border-collapse: collapse;
-}
-table,
-.table {
- overflow-y: hidden;
- width: 100%;
- font-size: 0.75rem;
-}
-.table .table-titles th {
- background-color: var(--lighter);
-}
-table > tbody > tr > td,
-table > tbody > tr > th,
-table > tfoot > tr > td,
-table > tfoot > tr > th,
-table > thead > tr > td,
-table > thead > tr > th,
-.table > .tbody > .tr > .td,
-.table > .tbody > .tr > .th,
-.table > .tfoot > .tr > .td,
-.table > .tfoot > .tr > .th,
-.table > .thead > .tr > .td,
-.table > .thead > .tr > .th,
-.table > .tr > .td.cbi-value-field,
-.table > .tr > .th.cbi-section-table-cell {
- padding: 0.5rem;
-}
-.container > .cbi-section:first-of-type > .table[width="100%"] > .tr > .td {
- padding: 0.6rem;
-}
-.cbi-section-table-cell {
- line-height: 1.1;
- align-self: flex-end;
- flex: 1 1 auto;
-}
-tr > td,
-tr > th,
-.tr > .td,
-.tr > .th,
-.cbi-section-table-row::before,
-#cbi-wireless > #wifi_assoclist_table > .tr:nth-child(2) {
- border-top: thin solid #ddd;
- padding: 1.1em 1.25rem;
-}
-#cbi-wireless .td,
-.table[width="100%"] > .tr:first-child > .td,
-[data-page="admin-network-diagnostics"] .tr > .td,
-.tr.table-titles > .th,
-.tr.cbi-section-table-titles > .th {
- border-top: 0 !important;
- background-color: #f6f9fc;
- padding: 1.1em 1.25rem;
- line-height: 1.3rem;
-}
-#cbi-network .tr:first-child > .td {
- border-top: 0;
-}
-.table[width="100%"] > .tr:first-child > .td {
- margin: auto 0;
-}
-.cbi-section-table-row {
- margin-bottom: 1rem;
- text-align: center !important;
- background: #f4f4f4;
-}
-.cbi-section-table-row:last-child {
- margin-bottom: 0;
-}
-.cbi-section-table-row > .cbi-value-field .cbi-dropdown,
-.cbi-section-table-row > .cbi-value-field .cbi-input-select,
-.cbi-section-table-row > .cbi-value-field .cbi-input-text,
-.cbi-section-table-row > .cbi-value-field .cbi-input-password {
- width: 100%;
-}
-.cbi-section-table-row > .cbi-value-field .cbi-input-text,
-.cbi-section-table-row > .cbi-value-field .cbi-input-password {
- min-width: 80px;
-}
-.cbi-section-table-row > .cbi-value-field [data-dynlist] > input,
-.cbi-section-table-row > .cbi-value-field input.cbi-input-password {
- width: calc(100% - 1.5rem);
-}
-.cbi-section-table-row .td {
- text-align: center !important;
-}
-.cbi-section-table-row .td .cbi-checkbox input[type="checkbox"] {
- margin: 0;
-}
-.control-group {
- display: inline-flex;
- width: 100%;
- flex-wrap: wrap;
- gap: 1rem;
-}
-.control-group input {
- border-right-width: 0;
- margin-right: 0;
-}
-.control-group input + button {
- border-bottom-left-radius: 0;
- border-top-left-radius: 0;
- margin-left: 0;
- border-left-width: 0;
-}
-.control-group:has(> input:first-child + .cbi-button) {
- gap: 0 !important;
-}
-.control-group:has(> input:first-child + .cbi-button) input {
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- width: 15.5rem;
- min-width: 15.5rem;
-}
-.control-group > * {
- vertical-align: middle;
-}
-div > table > tbody > tr:nth-of-type(2n),
-div > .table > .tr:nth-of-type(2n) {
- background-color: #f9f9f9;
-}
-/* fix multiple table */
-table table,
-.table .table,
-.cbi-value-field table,
-.cbi-value-field .table,
-td > table > tbody > tr > td,
-.td > .table > .tbody > .tr > .td,
-.cbi-value-field > table > tbody > tr > td,
-.cbi-value-field > .table > .tbody > .tr > .td {
- border: 0;
-}
-/* button style */
-.btn,
-.cbi-button,
-.item::after {
- font-size: 0.875rem;
- display: inline-block;
- width: auto !important;
- padding: 0.5rem 0.75rem;
- cursor: pointer;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- transition: all 0.2s ease-in-out;
- text-align: center;
- vertical-align: middle;
- white-space: nowrap;
- text-decoration: none;
- border: 0;
- border-radius: 0.25rem;
- background-color: #f0f0f0;
- background-image: none;
- appearance: none;
- -ms-touch-action: manipulation;
- touch-action: manipulation;
-}
-.btn:not(button) ul:not(.dropdown) li {
- padding: 0;
-}
-.cbi-button-up,
-.cbi-button-down {
- display: inline-block;
- min-width: 0;
- padding: 0.2rem 1rem;
- font-size: 0;
- color: transparent !important;
- background: url(../icon/arrow.svg) no-repeat center;
- background-size: 12px 20px;
-}
-.cbi-button-up {
- transform: scaleY(-1);
-}
-.cbi-button:not(select) {
- appearance: none !important;
-}
-.btn:hover,
-.btn:focus,
-.btn:active,
-.cbi-button:hover,
-.cbi-button:focus,
-.cbi-button:active,
-.item:hover::after,
-.item:focus::after,
-.item:active::after,
-.cbi-page-actions .cbi-button-apply + .cbi-button-save:hover,
-.cbi-page-actions .cbi-button-apply + .cbi-button-save:focus,
-.cbi-page-actions .cbi-button-apply + .cbi-button-save:active {
- text-decoration: none;
- outline: 0;
-}
-.btn:hover,
-.btn:focus,
-.cbi-button:hover,
-.cbi-button:focus,
-.item:hover::after,
-.item:focus::after {
- box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1);
-}
-.btn:active,
-.cbi-button:active,
-.item:active::after {
- box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1);
-}
-.cbi-button-up:hover,
-.cbi-button-up:focus {
- box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1);
-}
-.cbi-button-up:active {
- box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1);
-}
-.btn:disabled,
-.cbi-button:disabled {
- cursor: not-allowed;
- pointer-events: none;
- opacity: 0.5;
- box-shadow: none;
-}
-/* gray */
-.alert-message [class="btn"],
-.modal div[class="btn"],
-.cbi-button-find,
-.cbi-button-link,
-.cbi-button-up,
-.cbi-button-down,
-.cbi-button-neutral,
-.cbi-button[name="zero"],
-.cbi-button[name="restart"],
-.cbi-button[onclick="hide_empty(this)"] {
- color: #fff;
- border: thin solid #8898aa;
- background-color: #8898aa;
-}
-/* dark blue */
-.btn.primary,
-.cbi-page-actions .cbi-button-save,
-.cbi-page-actions .cbi-button-apply + .cbi-button-save,
-.cbi-button-add,
-.cbi-button-save,
-.cbi-button-positive,
-.cbi-button-link,
-.cbi-button[value="Enable"],
-.cbi-button[value="Scan"],
-.cbi-button[value^="Back"],
-.cbi-button-neutral[onclick="handleConfig(event)"] {
- font-weight: normal;
- color: #fff !important;
- border: thin solid #5e72e4;
- border: thin solid var(--primary);
- background-color: #5e72e4;
- background-color: var(--primary);
-}
-/* light blue */
-.cbi-page-actions .cbi-button-apply,
-.cbi-section-actions .cbi-button-edit,
-.cbi-button-edit,
-.cbi-button-apply,
-.cbi-button-reload,
-.cbi-button-action,
-.cbi-button[value="Submit"],
-.cbi-button[value="Upload"],
-.cbi-button[value$="Apply"],
-.cbi-button[onclick="addKey(event)"] {
- font-weight: normal;
- color: #fff !important;
- border: thin solid #5e72e4;
- border: thin solid var(--primary);
- background-color: #5e72e4;
- background-color: var(--primary);
-}
-/* red */
-.btn.danger,
-.cbi-section-remove > .cbi-button,
-.cbi-button-remove,
-.cbi-button-reset,
-.cbi-button-negative,
-.cbi-button[value="Stop"],
-.cbi-button[value="Kill"],
-.cbi-button[onclick="reboot(this)"],
-.cbi-button-neutral[value="Restart"] {
- font-weight: normal;
- color: #fff;
- border: thin solid #f5365c;
- border: thin solid var(--red);
- background-color: #f5365c;
- background-color: var(--red);
-}
-/* yellow */
-.btn[value="Dismiss"],
-.cbi-button[value="Terminate"],
-.cbi-button[value="Reset"],
-.cbi-button[value="Disabled"],
-.cbi-button[onclick^="iface_reconnect"],
-.cbi-button[onclick="handleReset(event)"],
-.cbi-button-neutral[value="Disable"] {
- font-weight: normal;
- color: var(--white);
- border: thin solid #eea236;
- background-color: #f0ad4e;
-}
-/* green */
-.cbi-button-success,
-.cbi-button-download {
- font-weight: normal;
- color: var(--white);
- border: thin solid #4cae4c;
- background-color: #5cb85c;
-}
-.cbi-page-actions .cbi-button-link:first-child {
- float: left;
-}
-.cbi-page-actions .cbi-dropdown .open {
- color: var(--white);
-}
-.cbi-page-actions .cbi-dropdown .more {
- display: block;
- width: 1px;
- height: 100%;
- overflow: hidden;
- text-indent: 5000px;
- background-color: var(--white);
-}
-.a-to-btn {
- text-decoration: none;
-}
-.cbi-value-field .cbi-button-add {
- font-weight: bold;
- padding: 1px 6px;
- display: inline-block;
- align-items: center;
-}
-.tabs {
- padding: 0 1rem;
- background-color: #FFFFFF;
- border-radius: 0.25rem;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03);
- white-space: nowrap;
- overflow-x: auto;
-}
-.tabs::-webkit-scrollbar {
- width: 1px;
- height: 5px;
-}
-.tabs::-webkit-scrollbar-thumb {
- background-color: #f6f9fc;
-}
-.tabs::-webkit-scrollbar-track {
- background-color: #fff;
-}
-.tabs li[class~="active"],
-.tabs li:hover {
- cursor: pointer;
- border-bottom: 0.18751rem solid #5e72e4;
- border-bottom: 0.18751rem solid var(--primary);
- color: #5e72e4;
- color: var(--primary);
- background-color: #e4e9ee;
- margin-bottom: 0;
- border-radius: 0;
-}
-.tabs li[class~="active"] a,
-.tabs li:hover a {
- color: #5e72e4;
- color: var(--primary);
-}
-.tabs li {
- font-size: 0.875rem;
- display: inline-block;
- padding: 0.875rem 0rem;
- border-bottom: 0.18751rem solid rgba(0, 0, 0, 0);
- margin: 0;
- transition: all 0.2s;
-}
-.tabs li a {
- text-decoration: none;
- color: #404040;
- padding: 0.5rem 0.8rem;
-}
-.tabs li:hover {
- border-bottom: 0.18751rem solid #5e72e4;
- border-bottom: 0.18751rem solid var(--primary);
-}
-.cbi-tabmenu {
- color: white;
- padding: 0.5rem 1rem 0 1rem;
- white-space: nowrap;
- overflow-x: auto;
-}
-.cbi-tabmenu::-webkit-scrollbar {
- width: 1px;
- height: 5px;
-}
-.cbi-tabmenu::-webkit-scrollbar-thumb {
- background-color: #f6f9fc;
-}
-.cbi-tabmenu::-webkit-scrollbar-track {
- background-color: #fff;
-}
-.cbi-tabmenu li {
- background: #dce3e9;
- display: inline-block;
- font-size: 0.875rem;
- border-top-left-radius: 0.25rem;
- border-top-right-radius: 0.25rem;
- padding: 0.5rem 0rem;
- border-bottom: 0.18751rem solid rgba(0, 0, 0, 0);
- margin: 0 0.2rem;
- position: relative;
-}
-.cbi-tabmenu li a {
- text-decoration: none;
- color: #404040;
- padding: 0.5rem 0.8rem;
-}
-.cbi-tabmenu li:hover {
- cursor: pointer;
- border-bottom: 0.18751rem solid #5e72e4;
- border-bottom: 0.18751rem solid var(--primary);
- color: #5e72e4;
- color: var(--primary);
- background-color: #e4e9ee;
- margin-bottom: 0;
-}
-.cbi-tabmenu li:hover a {
- color: #525f7f;
-}
-.cbi-tabmenu li[class~="cbi-tab"] {
- border-bottom: 0.18751rem solid #5e72e4;
- border-bottom: 0.18751rem solid var(--primary);
- color: #5e72e4;
- color: var(--primary);
- background-color: #e4e9ee;
- margin-bottom: 0;
-}
-.cbi-tabmenu li[class~="cbi-tab"] a {
- color: #5e72e4;
- color: var(--primary);
-}
-.cbi-tab-descr {
- padding: 0 1.25rem;
-}
-.cbi-section-node {
- display: flex;
- flex-direction: column;
- gap: 0.875rem;
-}
-.cbi-section-node > [id^="cbi-config-"] {
- display: flex;
- flex-direction: column;
- gap: 0.875rem;
-}
-.cbi-section-node > [id^="cbi-config-"]:first-child {
- padding-top: 1rem;
-}
-.cbi-section-node > [id^="cbi-config-"]:last-child {
- padding-bottom: 1rem;
-}
-.cbi-section .cbi-section-remove:nth-of-type(2n),
-.container > .cbi-section .cbi-section-node:nth-of-type(2n) {
- background-color: #f9f9f9;
-}
-[data-tab-title] {
- overflow: hidden;
- height: 0;
- opacity: 0;
- margin: 0;
- padding: 1rem 0;
- display: none !important;
-}
-[data-tab-title] p {
- margin-left: 1rem;
- margin-bottom: 1rem;
-}
-[data-tab-active="true"] {
- overflow: visible;
- height: auto;
- opacity: 1;
- display: inherit !important;
- transition: opacity 0.25s ease-in;
- margin: inherit !important;
-}
-.cbi-section[id] .cbi-section-remove:nth-of-type(4n+3),
-.cbi-section[id] .cbi-section-node:nth-of-type(4n+4) {
- background-color: #f9f9f9;
-}
-.cbi-section-node-tabbed {
- margin-top: 0;
- border: 0 solid #d4d4d4;
- border-radius: 0.25rem;
-}
-.cbi-section-node-tabbed > div {
- display: flex;
- flex-direction: column;
- gap: 0.875rem;
-}
-.cbi-tabcontainer > .cbi-value:nth-of-type(2n) {
- background-color: #f9f9f9;
-}
-.cbi-value-field {
- display: table-cell;
-}
-.cbi-value-field div + br {
- display: none;
-}
-.cbi-value-description {
- line-height: 1.25;
- display: table-cell;
- padding: 0.5rem 0;
- opacity: 0.5;
- font-size: 0.75rem;
-}
-.cbi-value-description abbr {
- color: #32325d;
- color: var(--gray-dark);
-}
-.cbi-value-description:empty {
- display: none;
-}
-.cbi-value-title {
- display: table-cell;
- float: left;
- width: 23rem;
- padding-right: 2rem;
- text-align: right;
- word-wrap: break-word;
-}
-.cbi-value {
- display: inline-block;
- width: 100%;
- padding: 0 1rem;
- line-height: 2.4rem;
-}
-.cbi-value ul {
- line-height: 1.25;
-}
-.cbi-value-field.cbi-dropdown-open .cbi-dropdown {
- border-color: var(--primary);
- box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1);
-}
-.cbi-value-field .cbi-dropdown,
-.cbi-value-field .cbi-input-select,
-.cbi-value input[type="text"],
-.cbi-value input[type="password"],
-.cbi-value textarea {
- min-width: 18rem;
-}
-#cbi-firewall-zone .cbi-input-select,
-#cbi-network-switch_vlan .cbi-input-select {
- min-width: 11rem;
-}
-#cbi-network-switch_vlan .cbi-input-text {
- max-width: 3rem;
-}
-.cbi-input-file {
- appearance: auto !important;
-}
-.cbi-input-file::-webkit-file-upload-button {
- display: inline-block !important;
-}
-.cbi-input-invalid {
- color: #f5365c !important;
- border-color: #f5365c !important;
-}
-.cbi-section-error {
- font-weight: bold;
- line-height: 1.42857143;
- margin: 18px;
- padding: 6px;
- border: thin solid #f5365c;
- border-radius: 3px;
- background-color: #fce6e6;
-}
-.cbi-section-error ul {
- margin: 0 0 0 20px;
-}
-.cbi-section-error ul li {
- font-weight: bold;
- color: #f5365c;
-}
-.td[data-title]::before {
- font-weight: bold;
- display: none;
- padding: 0.25rem 0;
- content: attr(data-title) ":\20";
- text-align: left;
- white-space: nowrap;
-}
-.tr.placeholder .td[data-title]::before {
- display: none;
-}
-.tr[data-title]::before,
-.tr.cbi-section-table-titles.named::before {
- font-weight: bold;
- display: table-cell;
- align-self: center;
- flex: 1 1 5%;
- padding: 0.25rem;
- content: attr(data-title) "\20";
- text-align: center;
- vertical-align: middle;
- white-space: normal;
- word-wrap: break-word;
- background-color: #f6f9fc;
-}
-.cbi-rowstyle-1 {
- background-color: #f9f9f9;
-}
-.cbi-rowstyle-2 {
- background-color: #eee;
-}
-.cbi-rowstyle-2 .cbi-button-up,
-.cbi-rowstyle-2 .cbi-button-down,
-body:not(.Interfaces) .cbi-rowstyle-2:first-child {
- background-color: #fff !important;
-}
-.cbi-section-table .cbi-section-table-titles .cbi-section-table-cell {
- width: auto !important;
-}
-.td.cbi-section-actions {
- text-align: right !important;
- vertical-align: middle;
-}
-.td.cbi-section-actions > * {
- display: inline-flex;
- gap: 1rem;
-}
-.td.cbi-section-actions > * > *,
-.td.cbi-section-actions > * > form > * {
- display: flex;
- align-items: center;
-}
-.td.cbi-section-actions > * > form {
- display: inline-flex;
- margin: 0;
-}
-.td .cbi-checkbox {
- justify-content: center;
-}
-.cbi-checkbox {
- display: flex;
- align-items: center;
- height: 2.5rem;
-}
-/* lists */
-.cbi-dynlist {
- line-height: 1.3;
- flex-direction: column;
- min-height: 30px;
- cursor: text;
- gap: 0.875rem;
-}
-.cbi-dynlist > .item {
- display: inline-flex;
- flex-wrap: nowrap;
- position: relative;
- max-width: 25rem;
- pointer-events: none;
- color: #8898aa;
- outline: 0;
-}
-.cbi-dynlist > .item::after {
- content: "\00D7";
- pointer-events: auto;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 2.5rem !important;
- height: 2.5rem;
- margin: 0;
- font-weight: normal;
- font-size: 1rem;
- line-height: 1.5rem;
- color: #fff;
- border: 1px solid #f5365c;
- border-radius: 0 0.25rem 0.25rem 0;
- outline: 0;
- background-color: var(--red);
- background-image: none;
- box-shadow: none;
- box-sizing: border-box;
-}
-.cbi-dynlist > .item > span {
- display: block;
- padding: 0.5rem 0.75rem;
- min-width: 15.5rem;
- width: 15.5rem;
- height: 2.5rem;
- overflow: hidden;
- text-overflow: ellipsis;
- user-select: text;
- white-space: nowrap;
- word-break: break-word;
- line-height: 1.5em;
- color: #8898aa;
- border: 1px solid #dee2e6;
- border-radius: 0.25rem 0 0 0.25rem;
- outline: 0;
- background-image: none;
- box-shadow: none;
- box-sizing: border-box;
- transition: box-shadow 0.15s ease;
-}
-.cbi-dynlist > .add-item {
- display: inline-flex;
- align-items: center;
- width: 100%;
- min-width: 15.5rem;
- flex-wrap: nowrap;
-}
-.cbi-dynlist > .add-item input {
- display: block;
- padding: 0.5rem 0.75rem;
- box-sizing: border-box;
- min-width: 15.5rem;
- width: 15.5rem;
- transition: box-shadow 0.15s ease;
- white-space: nowrap;
- word-break: break-word;
- font-size: 0.875rem;
- line-height: 1.5rem;
- color: #8898aa;
- border: 1px solid #dee2e6;
- border-radius: 0.25rem 0 0 0.25rem;
- border-right-width: 0;
- outline: 0;
- background-image: none;
- box-shadow: none;
-}
-.cbi-dynlist > .add-item .cbi-button {
- display: flex;
- width: auto !important;
- padding-left: 0.8rem;
- padding-right: 0.8rem;
- margin-left: 0;
- align-items: center;
- justify-content: center;
- font-size: 0.875rem;
- line-height: 1.5rem;
- outline: 0;
- background-image: none;
- background-color: var(--gray);
- box-shadow: none;
- color: var(--white);
- border-color: var(--gray);
- border-radius: 0.25rem;
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
-}
-.cbi-dynlist > .add-item .cbi-button-add {
- width: 2.5rem !important;
- padding: 0.5rem 0 !important;
- font-weight: normal;
- font-size: 1.2rem;
- color: #fff;
- background-color: var(--primary);
- border: 1px solid var(--primary);
-}
-.cbi-dynlist > .add-item:not([ondrop]) > input {
- overflow: hidden;
- min-width: 15.5rem;
- width: 15.5rem;
- white-space: nowrap;
- text-overflow: ellipsis;
-}
-.cbi-dynlist > .item.drag-over,
-.cbi-dynlist > .add-item:has(.drag-over) {
- border-top: 1px solid var(--red);
- margin-top: -1px;
-}
-.cbi-dynlist[name="sshkeys"] > .item {
- max-width: none;
-}
-.cbi-dynlist > .cbi-dynlist > .add-item[ondrop] > input {
- min-width: 13rem;
-}
-.cbi-dynlist,
-.cbi-dropdown {
- position: relative;
- display: inline-flex;
- padding-right: 0.25rem;
- min-height: 2.1875rem;
-}
-.cbi-dropdown {
- align-items: center;
-}
-.cbi-dropdown.btn > ul:not(.dropdown),
-.cbi-dropdown.cbi-button > ul:not(.dropdown) {
- margin: 0 0.75rem;
-}
-.cbi-dropdown[placeholder*="select"] {
- max-width: 25rem;
- height: auto;
- margin-top: -3px;
-}
-.cbi-dropdown > ul {
- display: flex;
- overflow-x: hidden;
- overflow-y: auto;
- width: 100%;
- padding: 0;
- list-style: none;
- outline: 0;
-}
-.cbi-dropdown > ul.preview {
- display: none;
-}
-.cbi-dropdown.cbi-button-apply,
-.cbi-dropdown.cbi-button-action {
- padding: 0;
-}
-.cbi-button-apply > ul.preview {
- display: none;
-}
-.cbi-button-apply > ul.preview li {
- color: #fff;
-}
-.cbi-button-apply > ul:first-child li {
- color: #fff;
-}
-.cbi-dropdown > .open {
- display: flex;
- align-items: center;
- width: 1rem;
- height: 100%;
- padding: 0 0.75rem;
- cursor: pointer;
- user-select: none;
- font-size: 0;
- color: #8898aa;
- background-color: currentColor;
- mask-image: var(--dropdown-arrow-icon);
- mask-repeat: no-repeat;
- mask-size: 100% 1rem;
- mask-position: center;
- mask-mode: match-source;
-}
-.cbi-dropdown > .more,
-.cbi-dropdown > ul > li[placeholder] {
- display: none;
-}
-.cbi-dropdown > ul > li {
- display: none;
- overflow: hidden;
- align-items: center;
- align-self: center;
- flex-grow: 1;
- flex-shrink: 1;
- min-height: 20px;
- padding: 0.125rem 0.25em;
- white-space: nowrap;
- text-overflow: ellipsis;
-}
-.cbi-dropdown > ul > li .hide-open {
- display: initial;
-}
-.cbi-dropdown > ul > li .hide-close {
- display: none;
-}
-.cbi-dropdown > ul > li[display]:not([display="0"]) {
- border-left: thin solid #ccc;
-}
-.cbi-dropdown[empty] > ul {
- max-width: 1px;
-}
-.cbi-dropdown > ul > li > form {
- display: none;
- margin: 0;
- padding: 0;
- pointer-events: none;
-}
-.cbi-dropdown > ul > li img {
- margin-right: 0.25em;
- vertical-align: middle;
-}
-.cbi-dropdown > ul > li > form > input[type="checkbox"] {
- height: auto;
- margin: 0;
-}
-.cbi-dropdown > ul > li input[type="text"] {
- height: 2rem;
- min-width: 16rem;
- padding: 0 0.5rem;
-}
-.cbi-dropdown[open] > ul.dropdown {
- position: absolute;
- z-index: 1100;
- display: block;
- width: auto;
- min-width: 100%;
- max-width: none;
- max-height: 200px !important;
- border: 0 solid #918e8c;
- background: #ffffff;
- box-shadow: 0 0 4px #918e8c;
- border-radius: 0.25rem;
- color: var(--main-menu-color);
- margin-left: 0 !important;
- margin-top: 0.25rem;
- left: 0;
-}
-.cbi-dropdown[open] > ul.dropdown li {
- color: #000;
-}
-.cbi-dropdown > ul > li[display],
-.cbi-dropdown[open] > ul.preview,
-.cbi-dropdown[open] > ul.dropdown > li,
-.cbi-dropdown[multiple] > ul > li > label,
-.cbi-dropdown[multiple][open] > ul.dropdown > li,
-.cbi-dropdown[multiple][more] > .more,
-.cbi-dropdown[multiple][empty] > .more {
- display: flex;
- align-items: center;
- flex-grow: 1;
-}
-.cbi-dropdown[empty] > ul > li,
-.cbi-dropdown[optional][open] > ul.dropdown > li[placeholder],
-.cbi-dropdown[multiple][open] > ul.dropdown > li > form {
- display: block;
-}
-.cbi-dropdown[open] > ul.dropdown > li .hide-open {
- display: none;
-}
-.cbi-dropdown[open] > ul.dropdown > li .hide-close {
- display: initial;
-}
-.cbi-dropdown[open] > ul.dropdown > li {
- border-bottom: thin solid #ccc;
- padding: 0.5rem 0.75rem;
- cursor: pointer;
-}
-.cbi-dropdown[open] > ul.dropdown > li label {
- margin-left: 0.5rem;
-}
-.cbi-dropdown[open] > ul.dropdown > li[selected] {
- background: #e4e9ee;
-}
-.cbi-dropdown[open] > ul.dropdown > li.focus {
- background: #e4e9ee;
- outline: none;
-}
-.cbi-dropdown[open] > ul.dropdown > li:last-child {
- margin-bottom: 0;
- border-bottom: 0;
-}
-.cbi-dropdown[open] > ul.dropdown > li[unselectable] {
- opacity: 0.7;
-}
-.cbi-dropdown[open] > ul.dropdown > li > input.create-item-input:first-child:last-child {
- width: 100%;
-}
-.cbi-dropdown[disabled] {
- pointer-events: none;
- opacity: 0.6;
-}
-.cbi-dropdown .zonebadge {
- width: 100%;
-}
-.cbi-dropdown[open] .zonebadge {
- width: auto;
-}
-/* progressbar */
-.cbi-progressbar {
- position: relative;
- display: flex;
- width: 100%;
- font-size: 0.75rem;
- background-color: #e9ecef;
- border-radius: 0.5rem;
- height: 1rem;
- overflow: hidden;
-}
-.cbi-progressbar > div {
- display: block;
- position: absolute;
- height: 100%;
- background-color: var(--primary);
- border-radius: 0.5rem;
- transition: width 0.3s;
-}
-.cbi-progressbar::after {
- content: attr(title);
- position: absolute;
- font-size: 0.75rem;
- color: var(--bs-heading-color);
- width: 100%;
- height: 100%;
- text-align: center;
- line-height: 1rem;
- z-index: 2;
-}
-#modal_overlay {
- position: fixed;
- z-index: 900;
- top: 0;
- right: 10000px;
- bottom: 0;
- left: -10000px;
- overflow-y: scroll;
- transition: opacity 0.125s ease-in;
- opacity: 0;
- background: rgba(0, 0, 0, 0.7);
- -webkit-overflow-scrolling: touch;
-}
-.modal {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- width: 90%;
- min-width: 270px;
- max-width: 600px;
- min-height: 32px;
- margin: 5em auto;
- padding: 1rem;
- border-radius: 0.25rem !important;
- background: #fff;
- box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12);
-}
-.modal > * {
- line-height: normal;
- flex-basis: 100%;
- margin-bottom: 0.5em;
- max-width: 100%;
-}
-.modal > pre,
-.modal > textarea {
- font-size: 1rem;
- font-size-adjust: 0.35;
- overflow: auto;
- padding: 0.5rem;
- cursor: auto;
- white-space: pre-wrap;
- color: var(--dark-primary);
- outline: 0;
- border-radius: 0.25rem;
- border: 1px solid var(--lighter);
- transition: all 0.2s ease;
-}
-.modal > pre:focus,
-.modal > textarea:focus {
- border: 1px solid var(--primary);
- box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1);
-}
-.modal > h4 {
- display: block;
- flex-grow: 1;
- max-width: none;
- padding: 1rem;
- margin: -1rem -1rem 0 -1rem;
- font-size: 1rem;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03);
- border-radius: 0.25rem 0 0 0.25rem;
-}
-.modal h5 {
- margin-top: 1rem;
- font-weight: 600;
-}
-.modal label > input[type="checkbox"] {
- top: 0;
-}
-.modal ul li {
- list-style-type: square;
- color: #808080;
-}
-.modal p {
- word-break: break-word;
- margin-top: 1rem;
-}
-.modal .label {
- font-size: 0.6rem;
- font-weight: normal;
- padding: 0.1rem 0.3rem;
- padding-bottom: 0;
- cursor: default;
- border-radius: 0;
-}
-.modal .label.warning {
- background-color: #f0ad4e !important;
-}
-.modal .btn {
- padding: 0.45rem 0.8rem;
-}
-.modal.cbi-modal {
- max-width: 90%;
- max-height: none;
-}
-.modal .cbi-map {
- margin-bottom: 1rem;
-}
-body.modal-overlay-active {
- overflow: hidden;
- height: 100vh;
-}
-body.modal-overlay-active #modal_overlay {
- right: 0;
- left: 0;
- opacity: 1;
-}
-.spinning {
- position: relative;
- padding-left: 32px !important;
-}
-.spinning::before {
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0.2em;
- width: 32px;
- content: "";
- background: url(/luci-static/resources/icons/loading.svg) no-repeat center;
- background-size: 16px;
-}
-/* luci */
-.hidden {
- display: none;
-}
-.left,
-.left::before {
- text-align: left !important;
-}
-.right,
-.right::before {
- text-align: right !important;
-}
-.button-row {
- display: flex;
- flex-direction: row;
- gap: 1rem;
- align-items: center;
- justify-content: flex-end;
- margin-bottom: 0 !important;
-}
-.center,
-.center::before {
- text-align: center !important;
-}
-.top {
- align-self: flex-start !important;
- vertical-align: top !important;
-}
-.bottom {
- align-self: flex-end !important;
- vertical-align: bottom !important;
-}
-.inline {
- display: inline;
-}
-.cbi-page-actions {
- padding: 1rem;
- justify-content: flex-end;
- display: flex;
- gap: 1rem;
-}
-.cbi-page-actions > form[method="post"] {
- display: inline-block;
-}
-.th[data-type="button"],
-.td[data-type="button"],
-.th[data-type="fvalue"],
-.td[data-type="fvalue"] {
- flex: 1 1 2em;
- text-align: center;
-}
-.ifacebadge {
- display: inline-flex;
- align-items: center;
- gap: 0.2rem;
- padding: 0.25rem 0.8rem;
- background: #eee;
- border-radius: 4px;
-}
-td > .ifacebadge,
-.td > .ifacebadge {
- font-size: 0.875rem;
- background-color: #f0f0f0;
-}
-.ifacebadge > em,
-.ifacebadge > img {
- display: inline-block;
- margin: 0 0.75rem;
-}
-.ifacebadge > img + img {
- margin: 0 0.2rem 0 0;
-}
-.network-status-table {
- display: flex;
- flex-wrap: wrap;
-}
-.network-status-table .ifacebox {
- flex-grow: 1;
- border-radius: 0.25rem;
- overflow: hidden;
- margin: 1rem;
-}
-.network-status-table .ifacebox-body {
- display: flex;
- flex-direction: column;
- height: 100%;
- gap: 0.5em;
-}
-.network-status-table .ifacebox-body > span {
- flex: 10 10 auto;
-}
-.network-status-table .ifacebox-body > div {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem;
- height: 100%;
-}
-.network-status-table .ifacebox-body .ifacebadge {
- align-items: center;
- flex: 1 1 auto;
- min-width: 220px;
- padding: 0.5em;
- background-color: #fff;
-}
-.network-status-table .ifacebox-body .ifacebadge > span {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-/* textarea */
-.cbi-input-textarea {
- font-family: inherit;
- width: 100%;
- font-size: 0.875rem;
- min-height: 14rem;
- padding: 0.8rem;
- color: #8898aa;
- border-radius: 0.25rem;
- border: 1px solid #dee2e6;
- min-width: 16rem;
-}
-#content_syslog {
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03);
-}
-#syslog {
- font-size: small;
- font-family: 'Google Sans';
- line-height: 1.25;
- overflow-y: hidden;
- width: 100%;
- min-height: 15rem;
- padding: 1rem;
- resize: none;
- color: #242424;
- border: 0;
- border-radius: 0.25rem;
- background-color: #ffffff;
-}
-#syslog:focus {
- outline: 0;
-}
-#cbi-ddns #syslog {
- overflow: auto;
-}
-/* config changes */
-.uci-change-list {
- font-family: inherit;
- overflow: scroll;
- width: 100%;
- display: flex;
- flex-direction: column;
- flex-wrap: wrap;
-}
-.uci-change-list ins,
-.uci-change-legend-label ins {
- display: block;
- padding: 2px;
- text-decoration: none;
- border: thin solid #0f0;
- background-color: #cfc;
-}
-.uci-change-list del,
-.uci-change-legend-label del {
- font-style: normal;
- display: block;
- padding: 2px;
- text-decoration: none;
- border: thin solid #f00;
- background-color: #fcc;
-}
-.uci-change-list var,
-.uci-change-legend-label var {
- font-style: normal;
- display: block;
- padding: 2px;
- text-decoration: none;
- border: thin solid #ccc;
- background-color: #eee;
-}
-.uci-change-list var ins,
-.uci-change-list var del {
- font-style: normal;
- padding: 0;
- white-space: pre;
- border: 0;
-}
-.uci-change-legend {
- padding: 5px;
-}
-.uci-change-legend-label {
- float: left;
- width: 150px;
-}
-.uci-change-legend-label > ins,
-.uci-change-legend-label > del,
-.uci-change-legend-label > var {
- display: block;
- float: left;
- width: 10px;
- height: 10px;
- margin-right: 4px;
-}
-.uci-change-legend-label var ins,
-.uci-change-legend-label var del {
- line-height: 0.4;
- border: 0;
-}
-.uci-change-list var,
-.uci-change-list del,
-.uci-change-list ins {
- padding: 0.5rem;
-}
-.uci-dialog .cbi-section {
- padding: 0.5rem;
-}
-.uci-dialog .cbi-section .uci-change-legend {
- line-height: 15px;
- padding: 10px 20px 0 20px;
-}
-.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label {
- padding: 0;
- margin: 0;
- position: relative;
- float: none;
- display: inline-block;
- width: 25%;
-}
-.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > ins,
-.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > del {
- width: 14px;
- height: 14px;
-}
-.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > var {
- position: relative;
- width: 14px;
- height: 14px;
-}
-.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > var ins,
-.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > var del {
- position: absolute;
- left: 2px;
- top: 2px;
- right: 2px;
- bottom: 2px;
-}
-.uci-dialog .cbi-section .uci-change-list {
- overflow: auto;
-}
-.uci-dialog .cbi-section .uci-change-list + .right .btn {
- color: #333;
-}
-.uci-dialog .cbi-section .uci-change-list + .right .cbi-dropdown ul:not(.dropdown) li {
- color: #fff;
-}
-.uci-dialog .cbi-section .uci-change-list + .right .cbi-button {
- padding: 0.45rem 0.8rem;
-}
-/* other fix */
-#iwsvg,
-#iwsvg2,
-#bwsvg {
- border: thin solid #d4d4d4 !important;
-}
-#iwsvg {
- border-top: 0 !important;
-}
-.ifacebox {
- line-height: 1.25;
- display: inline-flex;
- overflow: hidden;
- flex-direction: column;
- border-radius: 4px;
- min-width: 100px;
- background-color: #f9f9f9;
-}
-.ifacebox-head {
- padding: 0.25em;
- background: #eee;
-}
-.ifacebox-head.active {
- background: #5e72e4;
- background: var(--primary);
-}
-.ifacebox-head.active * {
- color: #fff;
- color: var(--white);
-}
-.ifacebox-body {
- padding: 0.875rem 1rem;
- line-height: 1.6em;
-}
-.cbi-image-button {
- margin-left: 0.5rem;
-}
-.zonebadge {
- display: inline-block;
- padding: 0.2rem 0.5rem;
- border-radius: 4px;
-}
-.zonebadge .ifacebadge {
- margin: 0.1rem 0.2rem;
- padding: 0.2rem 0.3rem;
- border: thin solid #6c6c6c;
-}
-.zonebadge > input[type="text"] {
- min-width: 10rem;
- margin-top: 0.3rem;
- padding: 0.16rem 1rem;
-}
-.zonebadge > em,
-.zonebadge > strong {
- display: inline-block;
- margin: 0 0.2rem;
-}
-.cbi-value-field .cbi-input-checkbox,
-.cbi-value-field .cbi-input-radio {
- margin-top: 0.1rem;
-}
-.cbi-value-field > ul > li {
- display: flex;
-}
-.cbi-value-field > ul > li > label {
- margin-top: 0.5rem;
-}
-.cbi-value-field > ul > li .ifacebadge {
- margin-top: -0.5rem;
- margin-left: 0.4rem;
- background-color: #eee;
-}
-.cbi-section-table-row > .cbi-value-field .cbi-dropdown {
- min-width: 3rem;
-}
-.cbi-section-create {
- display: inline-flex;
- align-items: center;
- padding: 0.875rem 1rem;
-}
-.cbi-section-create > div:first-child > input {
- width: 100%;
- border-right: none;
- border-bottom-right-radius: 0;
- border-top-right-radius: 0;
-}
-.cbi-section-create > div:first-child + button {
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
-}
-.cbi-section-remove {
- padding: 0.5rem 1rem;
-}
-div.cbi-value var,
-td.cbi-value-field var,
-.td.cbi-value-field var {
- font-style: italic;
- color: #0069d6;
-}
-.cbi-optionals {
- padding: 1rem 1rem 0 1rem;
- border-top: thin solid #ccc;
-}
-.cbi-dropdown-container {
- position: relative;
-}
-.cbi-tooltip-container,
-span[data-tooltip],
-span[data-tooltip] .label {
- cursor: help !important;
-}
-.cbi-tooltip {
- position: absolute;
- z-index: 1000;
- left: -10000px;
- box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1);
- border-radius: 0.25rem;
- background: #fff;
- white-space: pre;
- padding: 0.5rem;
- opacity: 0;
- transition: opacity 0.25s ease-in;
- transform: translate(-50%, 10%);
-}
-.cbi-tooltip-container:hover .cbi-tooltip:not(:empty) {
- left: auto;
- transition: opacity 0.25s ease-in;
- opacity: 1;
-}
-.zonebadge .cbi-tooltip {
- margin: -1.5rem 0 0 -0.5rem;
- padding: 0.25rem;
- background: inherit;
-}
-.zonebadge-empty {
- color: #404040;
- background: repeating-linear-gradient(45deg, rgba(204, 204, 204, 0.5), rgba(204, 204, 204, 0.5) 5px, rgba(255, 255, 255, 0.5) 5px, rgba(255, 255, 255, 0.5) 10px);
-}
-.zone-forwards {
- display: flex;
- min-width: 10rem;
-}
-.zone-forwards > * {
- flex: 1 1 45%;
-}
-.zone-forwards > span {
- flex-basis: 10%;
- padding: 0 0.25rem;
- text-align: center;
-}
-.zone-forwards .zone-src,
-.zone-forwards .zone-dest {
- display: flex;
- flex-direction: column;
-}
-.label {
- font-size: 0.875rem;
- font-weight: bold;
- padding: 0.3rem 0.8rem;
- white-space: nowrap;
- text-decoration: none;
- text-transform: uppercase;
- color: #fff !important;
- border-radius: 3px;
- background-color: #bfbfbf;
- text-shadow: none;
-}
-label > input[type="checkbox"],
-label > input[type="radio"] {
- position: relative;
- top: 0.4rem;
- right: 0.2rem;
- margin: 0;
- vertical-align: bottom;
-}
-label[data-index][data-depends] {
- padding-right: 2em;
-}
-.showSide {
- display: none;
-}
-.darkMask {
- position: fixed;
- z-index: 99;
- display: none;
- width: 100%;
- height: 100%;
- content: "";
- top: 0;
- background-color: rgba(0, 0, 0, 0.56);
- transition: all 0.2s;
-}
-.darkMask.active {
- display: block;
-}
-/* diagnostics */
-#diag-rc-output > pre,
-#command-rc-output > pre,
-[data-page="admin-services-wol"] .notice code {
- font-size: 1.2rem;
- font-size-adjust: 0.35;
- line-height: normal;
- display: block;
- overflow-y: hidden;
- width: 100%;
- padding: 8.5px;
- white-space: pre;
- color: #eee;
- background-color: #101010;
- box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12);
-}
-input[name="ping"],
-input[name="traceroute"],
-input[name="nslookup"] {
- width: 80%;
-}
-.controls {
- gap: 0.5rem;
- margin: 0 1.25rem !important;
-}
-.controls > div {
- gap: 0.5rem;
-}
-.controls > * > .btn:not([aria-label$="page"]) {
- flex-grow: initial !important;
- margin-top: 0.25rem;
-}
-.controls > #pager > .btn[aria-label$="page"] {
- font-size: 1.4rem;
- font-weight: bold;
-}
-.controls > * > label {
- margin-bottom: 0.2rem;
-}
-.td.version,
-.td.size {
- white-space: normal !important;
- word-break: break-word;
-}
-.cbi-tabmenu + .cbi-section {
- margin-top: 0;
-}
-.cbi-tab-disabled[data-errors]::after {
- position: absolute;
- top: -0.25rem;
- right: -0.25rem;
- content: attr(data-errors);
- background-color: var(--red);
- color: #fff;
- width: 0.875rem;
- height: 0.875rem;
- border-radius: 0.875rem;
- text-align: center;
- display: inline-flex;
- flex-direction: column;
- justify-content: center;
- font-size: 0.75em;
-}
-/* custom commands */
-.commands {
- display: grid !important;
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
- gap: 0.75rem;
-}
-.commandbox {
- padding: 0.5rem 1rem;
- gap: 0.5rem;
- border-bottom: thin solid #ccc;
- border-radius: 0.25rem;
- background: #eee;
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
-}
-.commandbox h3 {
- line-height: normal !important;
- overflow: hidden;
- margin: 6px 0 !important;
- white-space: nowrap;
- text-overflow: ellipsis;
-}
-.commandbox div {
- left: auto !important;
-}
-.commandbox code {
- overflow: hidden;
- max-width: fit-content;
- padding: 2px 3px;
- white-space: nowrap;
- text-overflow: ellipsis;
-}
-.commandbox code:hover {
- overflow-y: auto;
- max-height: 50px;
- white-space: normal;
-}
-.commandbox p:first-of-type {
- margin-top: -6px;
-}
-.commandbox p:nth-of-type(2) {
- margin-top: 2px;
-}
-#command-rc-output .alert-message {
- line-height: 1.42857143;
- position: absolute;
- top: 40px;
- right: 32px;
- max-width: 40%;
- margin: 0;
- animation: anim-fade-in 1.5s forwards;
- word-break: break-word;
- opacity: 0;
-}
-@keyframes anim-fade-in {
- 100% {
- opacity: 1;
- }
-}
-/* other fix */
-.fb-container .cbi-button {
- height: auto !important;
-}
-#cbi-usb_printer-printer em {
- display: block;
- padding: 1rem;
- text-align: center;
-}
-pre.command-output {
- padding: 1.5rem;
-}
-/* page fix */
-[data-page="admin-system-autoreboot"] #cbi-autoreboot {
- margin-top: 0;
-}
-[data-page="admin-system-system"] .control-group {
- gap: 0.875rem;
-}
-[data-page="admin-system-system"] .cbi-dynlist {
- margin: 0.25rem 0;
-}
-[data-page="admin-system-admin"] .cbi-map h2,
-[data-page="admin-system-admin-password"] .cbi-map h2,
-[data-page="admin-system-admin"] .cbi-map .cbi-map-descr,
-[data-page="admin-system-admin-password"] .cbi-map .cbi-map-descr {
- margin-left: 0;
- color: var(--gray-dark);
-}
-[data-page="admin-system-admin"] .cbi-section-node,
-[data-page="admin-system-admin-password"] .cbi-section-node {
- padding: 1rem 0;
-}
-[data-page="admin-system-admin-sshkeys"] .cbi-dynlist {
- margin-left: 1rem;
-}
-[data-page="admin-system-startup"] #view > div:first-child div[data-initialized="true"] {
- margin-top: -0.4375rem;
-}
-[data-page="admin-system-startup"] [data-tab-title] p {
- margin-left: 0;
- margin-bottom: 0;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-[data-page="admin-system-startup"] textarea {
- line-height: 1.25;
- overflow-y: auto;
- width: 100%;
- min-height: 15rem;
- margin: 0 1rem;
- padding: 1rem;
- resize: none;
- color: #8898aa;
- border-radius: 0.25rem;
- border: 1px solid #dee2e6;
-}
-[data-page="admin-system-startup"] textarea:focus-visible {
- outline: none;
- box-shadow: none;
- border: 1px solid var(--primary);
-}
-[data-page="admin-system-crontab"] #view p textarea {
- line-height: 1.25;
- overflow-y: hidden;
- width: 100%;
- min-height: 15rem;
- padding: 1rem;
- resize: none;
- background-color: transparent;
- background: var(--white);
- outline: none;
- color: #8898aa;
- border-radius: 0.25rem;
- border: 1px solid #dee2e6;
- transition: all 0.2s ease;
-}
-[data-page="admin-system-crontab"] #view p textarea:focus-visible {
- outline: none;
- box-shadow: none;
- border: 1px solid var(--primary);
-}
-[data-page="admin-system-crontabhelper"] .crontab-row .dropdown-container {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 0.5rem;
-}
-[data-page="admin-system-crontabhelper"] .crontab-row .dropdown-container div {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 0.5rem;
-}
-[data-page="admin-system-partexp-global"] .cbi-section {
- padding: 1rem 0;
-}
-[data-page="admin-system-attendedsysupgrade"] #view .cbi-button {
- margin-left: 0 !important;
- margin-top: 1rem !important;
-}
-[data-page="admin-system-attendedsysupgrade-configuration"] .cbi-map .cbi-map-descr {
- padding-bottom: 0;
-}
-[data-page="admin-system-flash"] .cbi-value {
- padding: 0 1rem;
-}
-[data-page="admin-system-flash"] .cbi-section .cbi-section {
- margin-top: 0;
-}
-[data-page="admin-system-flash"] .cbi-map-tabbed {
- border-radius: 0.25rem;
-}
-[data-page="admin-system-flash"] .cbi-section-node {
- padding-top: 0;
- padding-bottom: 0.5rem;
-}
-[data-page="admin-system-flash"] legend {
- font-size: 1.2rem;
- width: 100%;
- display: block;
- border-bottom: 1px solid rgba(0, 0, 0, 0.05);
- line-height: 1.5;
- margin-bottom: 0;
- letter-spacing: 0.1rem;
- color: #32325d;
- font-weight: bold;
- padding: 1rem 0 1rem 1rem;
-}
-[data-page="admin-system-flash"] .cbi-section-descr {
- font-weight: 600;
- padding: 1rem 0 1rem 1rem;
- color: #525f7f;
-}
-[data-page="admin-system-flash"] .cbi-page-actions {
- padding: 0rem 1rem 1rem 0rem;
-}
-[data-page="admin-system-flash"] .modal label > input[type="checkbox"] {
- top: -0.25rem;
-}
-[data-page="admin-system-flash"] .modal .btn {
- white-space: normal !important;
-}
-[data-page="admin-system-flash"] .modal label > input[type="checkbox"] {
- vertical-align: text-top;
- top: auto;
-}
-[data-page="admin-system-filetransfer"] #cbi-upload {
- margin-top: 0;
-}
-[data-page="admin-system-filetransfer"] .cbi-section-table {
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03);
-}
-[data-page="admin-system-fileassistant"] #upload-toggle {
- display: none !important;
-}
-[data-page="admin-system-fileassistant"] .fb-container .panel-title {
- padding: 0.5rem 0.75rem !important;
-}
-[data-page="admin-system-fileassistant"] .cbi-section.fb-container {
- padding: 0.5rem;
-}
-[data-page="admin-system-fileassistant"] .cbi-section.fb-container label.cbi-value-title {
- line-height: 1.5rem;
- padding: 0.5rem 0.75rem;
- width: 60px;
- text-align: left;
-}
-[data-page="admin-system-fileassistant"] .cbi-section.fb-container div.cbi-value-field {
- width: 100%;
- display: block;
- padding-left: 60px;
-}
-[data-page="admin-system-fileassistant"] .cbi-section.fb-container div#list-content table.cbi-section-table thead td.cbi-section-table-cell,
-[data-page="admin-system-fileassistant"] .cbi-section.fb-container div#list-content table.cbi-section-table tbody td.cbi-section-table-cell {
- width: 232px;
-}
-[data-page="admin-system-fileassistant"] .cbi-section.fb-container div#list-content tbody tr:nth-child(1) td.parent-icon strong {
- margin-left: 0 !important;
-}
-[data-page="admin-system-fileassistant"] .cbi-section.fb-container div#list-content tbody td.cbi-value-field.file-icon strong,
-[data-page="admin-system-fileassistant"] .cbi-section.fb-container div#list-content tbody td.cbi-value-field.folder-icon strong {
- vertical-align: middle;
- margin-left: 5px;
-}
-[data-page="admin-system-fileassistant"] .fb-container .panel-container {
- border-bottom-color: #dee2e6;
- display: flex;
-}
-[data-page="admin-system-fileassistant"] .fb-container .panel-container .panel-title {
- flex: 1;
-}
-[data-page="admin-system-fileassistant"] .fb-container .upload-container {
- display: flex !important;
-}
-[data-page="admin-system-fileassistant"] .fb-container .upload-container .upload-file {
- flex: 1;
- margin-right: 0 !important;
-}
-[data-page="admin-system-opkg"] h2 {
- margin-left: 0;
- color: var(--gray-dark);
-}
-[data-page="admin-system-opkg"] input[name="filter_i18n"] {
- top: 2px;
- vertical-align: text-top;
-}
-[data-page="admin-system-opkg"] div.btn {
- line-height: 3;
- display: inline;
- padding: 0.3rem 0.6rem;
-}
-[data-page="admin-system-opkg"] #maincontent > .container {
- margin: 0 1.25rem 1rem 1.25rem;
- margin-bottom: 1rem;
-}
-[data-page="admin-system-package-manager"] .controls div:nth-child(5) > label {
- margin-bottom: 5px;
-}
-[data-page="admin-system-package-manager"] .controls div:nth-child(5) > div label {
- margin-top: 1rem;
-}
-[data-page="admin-system-package-manager"] .controls div:nth-child(5) > div label input {
- top: -2px;
- right: 0;
- vertical-align: middle;
-}
-[data-page="admin-system-reboot"] p {
- padding-left: 1.5rem;
-}
-[data-page="admin-system-reboot"] p > span {
- position: relative;
- top: 0.1rem;
- left: 1rem;
-}
-[data-page="admin-system-reboot"] .cbi-button {
- background: #fb6340 !important;
- border-color: #fb6340 !important;
- margin-left: 0 !important;
-}
-[data-page="admin-system-reboot"] .cbi-button.cbi-button-action.important {
- align-self: flex-end;
-}
-/* applyreboot fix */
-#applyreboot-container {
- margin: 2rem;
-}
-#applyreboot-section {
- line-height: 300%;
- margin: 2rem;
-}
-[data-page="admin-system-poweroff"] .container h2 + br + p {
- margin-bottom: 1rem;
- padding-left: 1.5rem;
-}
-[data-page="admin-system-poweroffdevice"] .container h2 {
- margin: 0;
-}
-[data-page="admin-system-poweroffdevice"] .container h2 + p {
- margin-bottom: 1rem;
- padding-left: 1.5rem;
-}
-[data-page="admin-system-poweroffdevice"] .container button + div {
- display: none;
-}
-[data-page="admin-system-poweroffdevice"] .btn.cbi-button.cbi-button-negative {
- align-self: flex-end;
-}
-[data-page="admin-network-network"] .cbi-value-field .cbi-dynlist {
- padding: 0 !important;
-}
-[data-page="admin-network-network"] .td > .ifacebadge > .cbi-tooltip-container {
- display: flex;
-}
-[data-page="admin-network-network"] .td > .ifacebadge > .cbi-tooltip-container img {
- vertical-align: middle;
-}
-[data-page="admin-network-network"] div[data-name="_gen_server_keypair"] .cbi-value-title,
-[data-page="admin-network-network"] div[data-name="_gen_peer_keypair"] .cbi-value-title,
-[data-page="admin-network-network"] div[data-name="_gen_psk"] .cbi-value-title {
- height: 2.4rem;
-}
-[data-page="admin-network-network"] #modal_overlay > .modal.cbi-modal > div > p > textarea {
- height: 20em !important;
- border: 1px solid #dee2e6 !important;
- border-radius: 4px;
-}
-[data-page="admin-network-dhcp"] .cbi-value {
- padding: 0;
-}
-[data-page="admin-network-dhcp"] [data-tab-active="true"] {
- padding: 1rem 1rem !important;
-}
-[data-page="admin-network-diagnostics"] .table {
- box-shadow: none;
-}
-[data-page="admin-network-diagnostics"] .cbi-section {
- padding: 1rem;
- font-family: monospace;
- background: #fff !important;
-}
-[data-page="admin-network-diagnostics"] textarea {
- background: transparent;
- border-radius: 0.25rem;
- color: #8898aa;
- border: 1px solid #dee2e6;
- padding: 0.5rem;
-}
-[data-page="admin-network-diagnostics"] .tr {
- display: flex;
- flex-direction: row;
-}
-[data-page="admin-network-diagnostics"] .tr .td {
- background-color: #fff !important;
- border-bottom: 1px solid #dee2e6 !important;
- display: flex;
- flex-direction: row;
- align-items: center;
- flex: 1;
-}
-[data-page="admin-network-diagnostics"] .tr .td input {
- margin: 0 !important;
- border-right: none !important;
- border-bottom-right-radius: 0 !important;
- border-top-right-radius: 0 !important;
-}
-[data-page="admin-network-diagnostics"] .tr .td .cbi-dropdown,
-[data-page="admin-network-diagnostics"] .tr .td .cbi-button {
- height: 2.5rem;
- border-left: none !important;
- border-bottom-left-radius: 0 !important;
- border-top-left-radius: 0 !important;
-}
-[data-page="admin-network-diagnostics"] .tr .td .cbi-dropdown .more,
-[data-page="admin-network-diagnostics"] .tr .td .cbi-button .more {
- display: block;
- width: 1px;
- height: 100%;
- overflow: hidden;
- text-indent: 5000px;
- background-color: var(--white);
-}
-[data-page="admin-network-diagnostics"] .cbi-dropdown .open {
- color: var(--white);
-}
-[data-page="admin-network-firewall-custom"] #view p,
-[data-page="admin-status-routes"] #view p {
- padding: 0 1.25rem;
-}
-[data-page="admin-network-firewall-custom"] #view p textarea,
-[data-page="admin-status-routes"] #view p textarea {
- padding: 1rem;
- border-radius: 0.25rem;
-}
-[data-page="admin-network-firewall-custom"] #view > h3,
-[data-page="admin-status-routes"] #view > h3 {
- border-radius: 0.25rem 0.25rem 0 0;
-}
-[data-page="admin-network-firewall-custom"] #view .cbi-tabmenu + div,
-[data-page="admin-status-routes"] #view .cbi-tabmenu + div {
- margin-top: -0.5rem;
-}
-.cbi-section.fade-in .cbi-title {
- position: relative;
-}
-.cbi-section.fade-in .cbi-title > div:last-child {
- position: absolute;
- right: 1.25rem;
- top: 0;
-}
-.cbi-section.fade-in .cbi-title > div:last-child span {
- font-size: 0;
- background: none!important;
- color: #ced4da !important;
-}
-.cbi-section.fade-in .cbi-title > div:last-child span:after {
- transition: all 0.3s;
- font-family: 'argon' !important;
- content: '\e90f';
- font-size: 1.1rem;
- padding: 0.8755rem 0;
- position: absolute;
- right: 0;
-}
-.cbi-section.fade-in .cbi-title > div:last-child span[data-style=inactive]:after {
- transform: rotate(90deg);
-}
-[data-page="admin-status"] #view > div:first-child,
-[data-page="admin-status-overview"] #view > div:first-child {
- gap: 0 !important;
-}
-[data-page="admin-status-iptables"] .right {
- margin-bottom: 0 !important;
-}
-[data-page="admin-status-nftables"] .nft-chain-hook {
- padding: 0.5rem 1.25rem;
-}
-[data-page="admin-nlbw-display"] .cbi-section[data-tab="export"] {
- padding: 1.5rem !important;
-}
-[data-page="admin-nlbw-backup"] form {
- padding-left: 1.5rem;
-}
-[data-page="admin-services-ddns"] #syslog {
- overflow: auto;
- font-size: 12px !important;
-}
-[data-page="admin-services-ttyd"] .container {
- display: flex;
- flex-direction: column;
-}
-[data-page="admin-services-ttyd"] #view {
- flex: 1;
-}
-[data-page="admin-services-ttyd"] #view iframe {
- height: 100%;
-}
-#cbi-dockerd .cbi-section {
- box-sizing: border-box;
- padding: 16px;
-}
-#cbi-dockerd .cbi-section.cbi-tblsection {
- padding: 0;
-}
-#cbi-docker .cbi-section .cbi-section-node {
- box-sizing: border-box;
- padding: 16px;
-}
-[data-page="admin-services-npc"] div[id="cbid.npc.config.vkey"] {
- white-space: normal;
-}
-[data-page="admin-services-npc"] div[id="cbid.npc.config.vkey"] input[id="widget.cbid.npc.config.vkey"][type="password"] {
- margin-top: 0;
- margin-bottom: 0;
-}
-[data-page="admin-services-npc"] div[id="cbid.npc.config.vkey"] .cbi-button {
- margin: 0;
- border: none;
-}
-[data-page="admin-services-npc"] div[id^="cbid.npc."][id$=".server_addr"]:not([id="cbid.npc.config.server_addr"]) + br,
-[data-page="admin-services-npc"] div[id^="cbid.npc."][id$=".vkey"]:not([id="cbid.npc.config.vkey"]) + br,
-[data-page="admin-services-npc"] div[id^="cbid.npc."][id$=".protocol"]:not([id="cbid.npc.config.protocol"]) + br,
-[data-page="admin-services-npc"] div[id^="cbid.npc."][id$=".dns"]:not([id="cbid.npc.config.dns"]) + br {
- display: none;
-}
-[data-page="admin-services-npc"] div.cbi-value.nowrap {
- white-space: normal;
-}
-[data-page^="admin-services-openclash"] .oc {
- --bg-light: #fff;
- --bg-gray: #fff;
- border-radius: 8px;
-}
-[data-page^="admin-services-openclash"] .oc .developer-container {
- background: transparent;
-}
-[data-page^="admin-services-openclash"] #cbi-openclash > fieldset {
- background: none;
- padding: 0;
-}
-[data-page^="admin-services-openclash"] #cbi-openclash > fieldset > table > tbody > tr > td {
- border: none;
- padding: 0;
-}
-[data-page^="admin-services-openclash"] #cbi-openclash > fieldset .main-card {
- border: none;
- box-shadow: none;
- padding: 0 10px 10px 10px;
- background: transparent;
-}
-[data-page^="admin-services-openclash"] #cbi-openclash > fieldset .myip-main-card {
- margin: 0;
- padding: 10px;
- border: none;
- background: transparent;
-}
-[data-page^="admin-services-openclash"] #cbi-openclash > fieldset .myip-main-card .myip-section-title {
- margin: 0;
- border-width: 1px;
-}
-[data-page^="admin-services-openclash"] .sub-card .card-title {
- margin-top: 5px;
-}
-[data-page^="admin-services-openclash"] .oc .main-cards-container {
- margin: 0 !important;
- gap: 0 !important;
-}
-[data-page^="admin-services-openclash"] .oc .config-file-bottom {
- margin-bottom: 5px;
-}
-[data-page^="admin-services-openclash"] .oc .config-file-bottom .card-actions {
- margin-bottom: 2px !important;
-}
-[data-page^="admin-services-openclash"] .oc .card-content {
- align-items: center !important;
- justify-content: center !important;
-}
-[data-page^="admin-services-openclash"] .oc .card-controls {
- align-self: center;
-}
-[data-page^="admin-services-openclash"] .oc .core-main-controls {
- justify-content: center !important;
-}
-[data-page^="admin-services-openclash"] .oc .plugin-toggle-container {
- margin-left: inherit !important;
-}
-[data-page^="admin-services-openclash"] .oc .core-status-toggle {
- flex: 0 !important;
- min-width: auto !important;
-}
-[data-page^="admin-services-openclash"] .oc .config-upload-content {
- background: #fff !important;
-}
-[data-page^="admin-services-openclash"] .oc .subscription-info-container {
- background: transparent !important;
-}
-[data-page^="admin-services-openclash"] .cbi-tabmenu > li {
- border-right: none !important;
- margin: 0 0.4rem 0 0 !important;
-}
-[data-page^="admin-services-openclash"] .cbi-tabmenu > li:last-child {
- margin-right: 0 !important;
-}
-[data-page^="admin-services-openclash"] #tab-content .dom {
- padding: 0 1rem 1rem 1rem;
-}
-[data-page^="admin-services-openclash"] .cbi-input-file {
- padding: 0.2813rem;
- width: 15rem !important;
-}
-[data-page^="admin-services-openclash"] [id="container.openclash.config.debug"] fieldset {
- border: none !important;
- padding: 1rem !important;
-}
-[data-page^="admin-services-openclash"] #diag-rc-output > pre,
-[data-page^="admin-services-openclash"] #dns-rc-output > pre {
- font-size: 0.875rem;
- color: #8898aa;
- border: 1px solid #dee2e6;
- background-color: transparent;
- border-radius: 0.25rem;
- font-family: 'Google Sans' !important;
- box-shadow: none;
-}
-[data-page^="admin-services-openclash"] #debug-rc-output > textarea {
- font-family: 'Google Sans' !important;
-}
-[data-page^="admin-services-openclash"] .CodeMirror {
- font-size: inherit;
- font-family: 'Google Sans' !important;
-}
-[data-page^="admin-services-openclash"] .cbi-button-up,
-[data-page^="admin-services-openclash"] .cbi-button-down {
- padding: 0.8rem 1.5rem;
- background-color: #f1f1f1;
- font-size: 0;
-}
-[data-page^="admin-services-openclash"] select#CORE_VERSION,
-[data-page^="admin-services-openclash"] select#RELEASE_BRANCH {
- width: auto;
-}
-[data-page="admin-vpn-passwall"] h4 {
- background: transparent;
-}
-/* openvpn bug fix */
-.OpenVPN a {
- line-height: initial !important;
-}
-/* wireless overview */
-#cbi-wireless > #wifi_assoclist_table > .tr {
- box-shadow: inset 1px -1px 0 #ddd, inset -1px -1px 0 #ddd;
-}
-#cbi-wireless > #wifi_assoclist_table > .tr.placeholder > .td {
- right: 2rem;
- bottom: 2rem;
- left: 2rem;
- border-top: thin solid #ddd !important;
-}
-#cbi-wireless > #wifi_assoclist_table > .tr.table-titles {
- box-shadow: inset 1px 0 0 #ddd, inset -1px 0 0 #ddd;
-}
-#cbi-wireless > #wifi_assoclist_table > .tr.table-titles > .th {
- border-bottom: thin solid #ddd;
- box-shadow: 0 -1px 0 0 #ddd;
-}
-#wifi_assoclist_table > .tr > .td[data-title="RX Rate / TX Rate"] {
- width: 23rem;
-}
-/* samba */
-#cbi-samba [data-tab="template"] .cbi-value-field {
- display: block;
-}
-#cbi-samba [data-tab="template"] .cbi-value-title {
- width: auto;
- padding-bottom: 0.6rem;
-}
-/* fix status */
-.node-status-overview > .main fieldset:nth-child(4) .td:nth-child(2),
-.node-status-processes > .main .table .tr .td:nth-child(3) {
- white-space: normal;
-}
-div[style*="display:grid;grid-template-columns:repeat"] {
- display: flex !important;
- justify-content: space-evenly !important;
- flex-wrap: wrap;
-}
-div[style*="display:grid;grid-template-columns:repeat"] .ifacebox {
- text-align: center;
- flex-basis: 100px;
-}
-div[style*="display:grid;grid-template-columns:repeat"] .ifacebox .ifacebox-body {
- font-size: 0.7rem;
- padding: 0.875rem;
-}
-div[style*="display:grid;grid-template-columns:repeat"] .ifacebox .ifacebox-body .cbi-tooltip-container {
- font-size: inherit !important;
-}
-/* firewall */
-#iptables {
- margin: 0;
-}
-.Firewall form {
- margin: 2rem 2rem 0 0;
- padding: 0;
- box-shadow: none;
-}
-#cbi-firewall-redirect table *,
-#cbi-network-switch_vlan table *,
-#cbi-firewall-zone table * {
- font-size: small;
-}
-#cbi-firewall-redirect table input[type="text"],
-#cbi-network-switch_vlan table input[type="text"],
-#cbi-firewall-zone table input[type="text"] {
- width: 5rem;
-}
-#cbi-firewall-redirect table select,
-#cbi-network-switch_vlan table select,
-#cbi-firewall-zone table select {
- min-width: 3.5rem;
-}
-#cbi-network-switch_vlan .th,
-#cbi-network-switch_vlan .td {
- flex-basis: 12%;
-}
-#cbi-network-switch_vlan .table {
- display: block;
-}
-#cbi-network-switch_vlan .td {
- width: 100%;
-}
-#cbi-firewall-zone .table {
- display: block;
-}
-#cbi-firewall-zone .td {
- width: 100%;
-}
-[data-page^="admin-system-commands"] .panel-title,
-[data-page^="command-cfg"] .mobile-hide,
-[data-page^="command-cfg"] .showSide {
- display: none;
-}
-#mwan3-service-status {
- display: flex;
- justify-content: center;
- align-items: center;
- flex-wrap: wrap;
-}
-#mwan3-service-status > .alert-message {
- position: static;
- transform: none;
-}
-/* responsive */
-@media all and (-ms-high-contrast: none) {
- .main > .main-left > .nav > .slide > .menu::before {
- top: 30.25%;
- }
- .main > .main-left > .nav > li:last-child::before {
- top: 20%;
- }
- .showSide::before {
- top: -12px;
- }
-}
-@media screen and (max-width: 1600px) {
- header > .fill > .container > #logo {
- margin: 0 2.5rem 0 0.5rem;
- }
- .main-left {
- width: calc(0% + 13rem);
- }
- .btn:not(button),
- .label {
- padding: 0.5rem 0.75rem;
- }
- .cbi-value-title {
- width: 15rem;
- padding-right: 0.6rem;
- }
- .cbi-value-field .cbi-dropdown,
- .cbi-value-field .cbi-input-select,
- .cbi-value input[type="text"],
- .cbi-value input[type="password"],
- .cbi-value textarea {
- min-width: 18rem;
- }
- #cbi-firewall-zone .cbi-input-select {
- min-width: 9rem;
- }
- .cbi-input-textarea {
- font-size: small;
- }
- .node-admin-status > .main fieldset li > a {
- padding: 0.3rem 0.6rem;
- }
-}
-@media screen and (max-width: 1366px) {
- header > .fill > .container {
- cursor: default;
- }
- .main-left {
- width: calc(0% + 13rem);
- }
- .tabs > li > a,
- .cbi-tabmenu > li > a {
- padding: 0.2rem 0.8rem;
- }
- .panel-title {
- font-size: 1.1rem;
- padding-bottom: 1rem;
- }
- table {
- width: 100% !important;
- }
- .table .cbi-input-text {
- width: 100%;
- }
- .cbi-value-field .cbi-dropdown,
- .cbi-value-field .cbi-input-select,
- .cbi-value input[type="text"],
- .cbi-value input[type="password"] {
- min-width: 16rem;
- }
- #cbi-firewall-zone .cbi-input-select {
- min-width: 5.5rem;
- }
- .main > .main-left > .nav > li,
- .main > .main-left > .nav > li > a,
- .main .main-left .nav > li > a:first-child,
- .main > .main-left > .nav > .slide > .menu,
- .main > .main-left > .nav > li > [data-title="Log_out"] {
- font-size: 0.9rem;
- }
- .main > .main-left > .nav > .slide > .slide-menu > li > a {
- font-size: 0.875rem;
- }
- #modal_overlay {
- top: 0rem;
- }
- [data-page="admin-network-firewall-forwards"] .table:not(.cbi-section-table) {
- display: block;
- }
- [data-page="admin-network-firewall-forwards"] .table:not(.cbi-section-table),
- [data-page="admin-network-firewall-rules"] .table:not(.cbi-section-table),
- [data-page="admin-network-hosts"] .table,
- [data-page="admin-network-routes"] .table {
- overflow-y: visible;
- }
- .btn:not(button),
- .cbi-button {
- font-size: 0.875rem;
- }
-}
-@media screen and (max-width: 1152px) {
- header > .fill > .container > #logo {
- display: none;
- }
- header > .fill > .container > .brand {
- position: relative;
- }
- html,
- .main {
- overflow-y: visible;
- }
- .main > .loading > span {
- top: 25%;
- }
- .main-left {
- width: calc(0% + 13rem);
- }
- body:not(.logged-in) .showSide {
- visibility: hidden;
- width: 0;
- margin: 0;
- }
- .node-main-login > .main .cbi-value-title {
- text-align: left;
- }
- .cbi-value-title {
- width: 12rem;
- padding-right: 1rem;
- }
- .cbi-value-field .cbi-dropdown,
- .cbi-value-field .cbi-input-select,
- .cbi-value input[type="text"] {
- width: 16rem;
- min-width: 16rem;
- }
- .cbi-value input[name^="pw"],
- .cbi-value input[data-update="change"]:nth-child(2) {
- width: 13rem !important;
- min-width: 13rem;
- }
- #diag-rc-output > pre,
- #command-rc-output > pre,
- [data-page="admin-services-wol"] .notice code {
- font-size: 1rem;
- }
- .table {
- display: block;
- }
- .Interfaces .table {
- overflow-x: hidden;
- }
- #packages.table {
- display: grid;
- }
- .tr {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- }
- .Overview .table[width="100%"] > .tr {
- flex-wrap: nowrap;
- }
- .tr.placeholder {
- border-bottom: thin solid #ddd;
- }
- .tr.placeholder > .td,
- #cbi-firewall .tr > .td,
- #cbi-network .tr:nth-child(2) > .td,
- .cbi-section #wifi_assoclist_table .tr > .td {
- border-top: 0;
- }
- .th,
- .td {
- display: inline-block;
- align-self: flex-start;
- flex: 2 2 10%;
- text-overflow: ellipsis;
- word-wrap: break-word;
- }
- .td select,
- .td input[type="text"] {
- width: 100%;
- word-wrap: normal;
- }
- .td [data-dynlist] > input,
- .td input.cbi-input-password {
- width: calc(100% - 1.5rem);
- }
- .td[data-type="button"],
- .td[data-type="fvalue"] {
- flex: 1 1 12.5%;
- text-align: left;
- }
- .th.cbi-value-field,
- .td.cbi-value-field,
- .th.cbi-section-table-cell,
- .td.cbi-section-table-cell {
- flex-basis: auto;
- padding-top: 1rem;
- }
- .cbi-section-table-row {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- justify-content: space-between;
- box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12);
- }
- .td.cbi-value-field,
- .cbi-section-table-cell {
- display: inline-block;
- flex: 10 10 auto;
- flex-basis: 50%;
- text-align: center;
- }
- .td.cbi-section-actions {
- vertical-align: bottom;
- }
- .tr.table-titles,
- .tr.cbi-section-table-titles,
- .tr.cbi-section-table-descr {
- display: none;
- }
- .tr[data-title]::before,
- .tr.cbi-section-table-titles.named::before {
- font-size: 0.9rem;
- display: block;
- flex: 1 1 100%;
- border-bottom: thin solid rgba(0, 0, 0, 0.26);
- background: #e9ecef;
- }
- .td[data-title],
- [data-page^="admin-status-realtime"] .td[id] {
- text-align: left;
- }
- .td[data-title]::before {
- display: block;
- }
- .cbi-button + .cbi-button {
- margin-left: 0;
- }
- .td.cbi-section-actions > * > *,
- .td.cbi-section-actions > * > form > * {
- margin: 2.1px 3px;
- }
- .Firewall form {
- position: static !important;
- margin: 0 0 2rem 0;
- padding: 2rem;
- box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12);
- }
- .Firewall form input {
- width: 100% !important;
- margin: 0;
- margin-top: 1rem;
- }
- .Firewall .center,
- .Firewall .center::before {
- text-align: left !important;
- }
- .btn:not(button),
- .cbi-button {
- font-size: 0.875rem;
- }
-}
-@media screen and (max-width: 768px) {
- body {
- font-size: 0.875rem;
- }
- .cbi-progressbar::after {
- font-size: 0.6rem;
- }
- .main-left {
- position: fixed;
- z-index: 100;
- width: 0;
- }
- .main-left.active {
- width: 13rem;
- }
- .main-right {
- width: 100%;
- }
- .main-right.active {
- overflow-y: hidden;
- }
- .darkMask.active {
- display: block;
- }
- .showSide {
- padding: 0.1rem;
- position: relative;
- z-index: 99;
- display: inline-block !important;
- }
- .showSide::before {
- font-family: 'argon' !important;
- font-style: normal !important;
- font-weight: normal !important;
- font-variant: normal !important;
- text-transform: none !important;
- line-height: 1;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- content: "\e20e";
- font-size: 1.7rem;
- }
- header > .fill > .container > .flex1 > .brand {
- display: inline-block;
- }
- .main > .main-left > .nav > .slide > .slide-menu > li > a {
- font-size: 0.875rem;
- }
-}
-@media screen and (max-width: 600px) {
- .mobile-hide {
- display: none;
- }
- #maincontent > .container {
- margin: 0 1rem 1rem 1rem;
- }
- .cbi-value-title {
- text-align: left;
- }
- .cbi-dynlist p {
- padding: 0.5rem 1rem;
- }
- body {
- overflow-x: hidden;
- }
- .node-main-login .main .main-right #maincontent .container .cbi-map .cbi-section .cbi-section-node .cbi-value .cbi-value-field {
- width: 16rem;
- }
- .node-main-login footer {
- display: none;
- }
- .tabs::-webkit-scrollbar,
- .cbi-tabmenu::-webkit-scrollbar {
- width: 0px;
- height: 0px;
- }
- .cbi-value-field,
- .cbi-value-description {
- display: block !important;
- padding-left: 0 !important;
- padding-right: 0 !important;
- }
- [data-page="admin-system-admin-password"] .cbi-value-field {
- display: table-cell !important;
- }
- .modal.cbi-modal {
- max-width: 100%;
- max-height: none;
- }
- .modal {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- width: 100%;
- min-width: 270px;
- max-width: 600px;
- min-height: 32px;
- margin: 5em auto;
- padding: 1em;
- border-radius: 3px !important;
- background: #fff;
- box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12);
- }
- .cbi-dropdown[open] > ul.dropdown {
- margin-bottom: 1rem;
- }
- .login-page .login-container footer {
- display: none;
- }
-}
-@media screen and (max-width: 480px) {
- .mobile-hide {
- display: none;
- }
- div[style*="display:grid;grid-template-columns:repeat"] .ifacebox {
- flex-basis: 80px;
- }
- div[style*="display:grid;grid-template-columns:repeat"] .ifacebox .ifacebox-body {
- padding: 0.875rem 0.5rem;
- font-size: 0.6rem;
- }
- .login-page .login-container {
- margin-left: 0rem !important;
- width: 100%;
- }
- .login-page .login-container .login-form .form-login .input-group::before {
- color: #525461;
- }
- .login-page .login-container .login-form .form-login .input-group input {
- color: #525461;
- border-bottom: white 1px solid;
- border-bottom: var(--white) 1px solid;
- border-radius: 0;
- }
-}
-@media screen and (min-width: 600px) {
- ::-webkit-scrollbar {
- width: 10px;
- height: 10px;
- }
- ::-webkit-scrollbar,
- ::-webkit-scrollbar-corner {
- background: transparent;
- }
- ::-webkit-scrollbar-thumb {
- background: var(--primary);
- border-radius: 10px;
- }
- ::-webkit-scrollbar-thumb:hover {
- background: var(--primary);
- }
- ::-webkit-scrollbar-thumb:active {
- background: var(--primary);
- }
-}
+:root{--primary:#5e72e4;--dark-primary:#483d8b;--header-color:#fff;--menu-bg-color:#ffffff;--menu-color:#5f6368;--main-menu-color:#202124;--red:#f5365c;--orange:#fb6340;--yellow:#ffd600;--green:#2dce89;--teal:#11cdef;--cyan:#2bffc6;--gray:#8898aa;--gray-dark:#32325d;--light:#ced4da;--lighter:#e9ecef;--success:#2dce89;--info:#11cdef;--warning:#fb6340;--danger:#f5365c;--light:#adb5bd;--dark:#212529;--default:#172b4d;--white:#fff;--darker:black;--background-color:#f4f5f7;--login-form-bg-color:rgba(244,245,247,0.8);--blur-radius:10px;--blur-opacity:.5;--blur-radius-dark:10px;--blur-opacity-dark:.5;--font-family-sans-serif:"Google Sans", "Microsoft Yahei", "WenQuanYi Micro Hei", "sans-serif", "Helvetica Neue", "Helvetica", "Hiragino Sans GB";--dropdown-arrow-icon:url("data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik01LjI5MyA5LjI5M2ExIDEgMCAwIDEgMS40MTQgMEwxMiAxNC41ODZsNS4yOTMtNS4yOTNhMSAxIDAgMSAxIDEuNDE0IDEuNDE0bC02IDZhMSAxIDAgMCAxLTEuNDE0IDBsLTYtNmExIDEgMCAwIDEgMC0xLjQxNCIvPjwvc3ZnPg==")}@font-face{font-family:'Google Sans';src:local('Google Sans'),local('GoogleSans-Regular'),url('../fonts/GoogleSans-Regular.woff2') format('woff2'),url('../fonts/GoogleSans-Regular.woff') format('woff');font-weight:normal;font-style:normal;font-display:swap}@font-face{font-family:'TypoGraphica';src:local('TypoGraphica'),url('../fonts/TypoGraphica.woff2') format('woff2'),url('../fonts/TypoGraphica.woff') format('woff');font-weight:normal;font-style:normal;font-display:swap}@font-face{font-family:'argon';src:url('../fonts/argon.woff2') format('woff2'),url('../fonts/argon.woff') format('woff');font-weight:normal;font-style:normal;font-display:swap}*{margin:0;padding:0;box-sizing:border-box}html{-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}html,body{margin:0;padding:0;height:100%;font-family:var(--font-family-sans-serif)}body{background-color:var(--background-color);color:var(--gray-dark);-webkit-tap-highlight-color:transparent;font-size:.875rem}main{display:block}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:normal;line-height:1.1 !important;color:inherit}h1{font-size:2rem;margin:.67em 0;padding-bottom:.5rem;border-bottom:thin solid var(--lighter)}h2{padding:1rem 1.25rem;font-size:1.25rem;font-weight:bold;color:var(--gray-dark);border-radius:.25rem;background:var(--white);box-shadow:0 4px 8px rgba(0,0,0,0.03)}h3{margin:0;padding:.8755rem 1.25rem;font-size:1.1rem;font-weight:bold;line-height:1;color:var(--gray-dark);border-radius:.25rem;background:var(--white);display:block;width:100%}h4{margin:0;padding:.75rem 1.25rem;font-size:.875rem;font-weight:bold;color:var(--gray-dark-400)}h4 em{padding:0 .5rem}h5{margin:2rem 0 0 0;padding-bottom:.5rem;font-size:1rem}a{background-color:transparent}abbr{cursor:help;text-decoration:underline;color:var(--primary)}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:inherit;font-size:inherit;padding:1px 3px;color:var(--dark);border-radius:.5rem;background:var(--lighter)}small{font-size:90%;line-height:1.42857143;white-space:normal}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}hr{box-sizing:content-box;height:0;margin:1rem 0;overflow:visible;opacity:.1;border-color:var(--lighter)}pre{font-family:monospace, monospace;font-size:1em;overflow:auto}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none;appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;appearance:button;font:inherit}input[type="checkbox"]{appearance:none !important;-webkit-appearance:none !important;border:1px solid var(--primary);width:1rem !important;height:1rem !important;padding:0;cursor:pointer;transition:all .2s}input[type="checkbox"]:checked{border:1px solid var(--primary);background-image:url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 8 8\'%3e%3cpath fill=\'%23fff\' d=\'M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z\'/%3e%3c/svg%3e') !important;background-color:var(--primary);background-size:70%;background-repeat:no-repeat;background-position:center}select{padding:.36rem .8rem;color:var(--gray-dark);border:thin solid var(--lighter);background-color:var(--white);background-image:none}textarea{padding:.25rem;overflow:auto}textarea:focus-visible{outline:none;border:1px solid var(--primary)}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}details{display:block}summary{display:list-item}template{display:none}progress{vertical-align:baseline}ul{line-height:normal}li{list-style-type:none}[disabled="disabled"]{pointer-events:none}::selection{background-color:var(--primary);color:var(--white)}::placeholder{color:var(--lighter)}a:link,a:visited,a:active{color:var(--primary);text-decoration:none}a:hover{text-decoration:underline}.pull-right{float:right}.pull-left{float:left}.nowrap:not(.td){white-space:nowrap}div[style="width:100%;height:300px;border:1px solid #000;background:#fff"]{border:0 !important}.pure-g{display:flex;flex-flow:row wrap;align-content:flex-start}.pure-u{display:inline-block;vertical-align:top}.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.col-1{flex:1 1 30px !important}.col-2{flex:2 2 60px !important}.col-3{flex:3 3 90px !important}.col-4{flex:4 4 120px !important}.col-5{flex:5 5 150px !important}.col-6{flex:6 6 180px !important}.col-7{flex:7 7 210px !important}.col-8{flex:8 8 240px !important}.col-9{flex:9 9 270px !important}.col-10{flex:10 10 300px !important}.table{position:relative;display:table}.tr{display:table-row}.thead{display:table-header-group}.tbody{display:table-row-group}.tfoot{display:table-footer-group}.td,.th{line-height:normal;display:table-cell;padding:.5em;text-align:center;vertical-align:middle}.th{font-weight:bold;white-space:nowrap}.tr.placeholder{height:4em}.tr.placeholder>.td{line-height:3;position:absolute;right:0;bottom:0;left:0;padding:.4rem 0 !important;text-align:center !important;background:inherit}.td[width="33%"]{padding:1.1em 1.5rem}.table[width="33%"],.th[width="33%"],.td[width="33%"]{width:33%}.table[width="100%"],.th[width="100%"],.td[width="100%"]{width:100%}.btn,button,select,input,.cbi-dropdown{line-height:1.5rem;height:2.5rem;padding:.5rem .75rem;color:#8898aa;border:1px solid #dee2e6;border-radius:.25rem;outline:0;background-image:none;box-shadow:none;transition:box-shadow .15s ease}select{padding-right:1.5rem;background-image:url("data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjODg5OGFhIiBkPSJNNS4yOTMgOS4yOTNhMSAxIDAgMCAxIDEuNDE0IDBMMTIgMTQuNTg2bDUuMjkzLTUuMjkzYTEgMSAwIDEgMSAxLjQxNCAxLjQxNGwtNiA2YTEgMSAwIDAgMS0xLjQxNCAwbC02LTZhMSAxIDAgMCAxIDAtMS40MTQiLz48L3N2Zz4=");background-size:1rem;background-position:right .5rem center;background-repeat:no-repeat;appearance:none}select:not([multiple="multiple"]):focus,input:not(.cbi-button):focus,.cbi-dropdown:focus{border-color:var(--primary);box-shadow:0 3px 9px rgba(50,50,9,0),3px 4px 8px rgba(94,114,228,0.1)}.cbi-dropdown,select[multiple="multiple"]{height:auto}.login-page{display:flex;height:100%;flex-direction:column}.login-page .video{position:absolute;top:0;left:0;height:100vh;width:100vw;display:flex;align-items:center;justify-content:center;background-color:var(--darker);overflow:hidden}.login-page .video video{width:100%;height:auto}.login-page .volume-control{position:fixed;top:1rem;right:1rem;width:1.5rem;height:1.5rem;z-index:5000;cursor:pointer;background:url(../img/volume_high.svg) no-repeat center / contain}.login-page .volume-control.mute{background-image:url(../img/volume_off.svg)}.login-page .main-bg{position:absolute;top:0;left:0;height:100vh;width:100vw;background:url(../img/blank.png) no-repeat center / cover;transition:all .5s ease}.login-page .login-container{z-index:10;margin-left:5%;display:flex;height:100vh;width:26rem;flex-direction:column;background-color:var(--white);backdrop-filter:blur(var(--blur-radius));background-color:rgba(244, 245, 247, var(--blur-opacity));box-shadow:rgba(0,0,0,0.75) 0 0 35px -5px}.login-page .login-container .login-form{position:absolute;top:0;left:0;height:100vh;width:100vw;display:flex;flex-direction:column;align-items:center;max-width:26rem}.login-page .login-container .login-form .brand{display:flex;align-items:center;justify-content:center;margin:50px auto 100px 50px;color:var(--default);text-decoration:none}.login-page .login-container .login-form .brand .icon{width:50px;height:auto;margin-right:25px}.login-page .login-container .login-form .brand .brand-text{margin-right:45px;font-size:1.25rem;font-weight:700;font-family:"TypoGraphica";word-break:break-word}.login-page .login-container .login-form .brand:hover{text-decoration:none}.login-page .login-container .login-form .form-login{width:100%;padding:20px 50px;box-sizing:border-box}.login-page .login-container .login-form .form-login .errorbox{padding-bottom:2rem;text-align:center;color:var(--warning)}.login-page .login-container .login-form .form-login .input-group{position:relative;margin-bottom:1.25rem}.login-page .login-container .login-form .form-login .input-group::before{font-family:'argon' !important;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;position:absolute;top:10px;left:10px;z-index:100;font-size:1.5rem;color:var(--default)}.login-page .login-container .login-form .form-login .input-group .border{position:absolute;bottom:0;width:100%;height:1px;border-bottom:1px solid var(--primary);transform:scaleX(0);transition:transform .3s ease}.login-page .login-container .login-form .form-login .input-group input{display:block;width:100%;margin:.825rem 0;padding:.5rem .75rem .5rem 3rem;font-size:1rem;line-height:1.5em;color:var(--default);background-color:transparent;background-clip:padding-box;border:0;border-bottom:1px solid var(--white);border-radius:0;outline:none;box-sizing:border-box;box-shadow:0 3px 2px rgba(233,236,239,0.05);transition:all .3s cubic-bezier(.68, -0.55, .265, 1.55)}.login-page .login-container .login-form .form-login .input-group input:focus+.border{transform:scaleX(1)}.login-page .login-container .login-form .form-login .input-group .cbi-input-password{position:relative;margin-bottom:2rem}.login-page .login-container .login-form .form-login .input-group.user-icon::before{content:"\e971"}.login-page .login-container .login-form .form-login .input-group.pass-icon::before{content:"\e910"}.login-page .login-container .login-form .cbi-button-apply{width:100% !important;min-height:50px;margin:30px 0 100px;padding:10px 0;font-size:15px;font-weight:600;color:var(--white);text-align:center;letter-spacing:.8rem;background-color:var(--primary) !important;border:none;border-radius:6px;outline:none;cursor:pointer;box-shadow:rgba(0,0,0,0.1) 0 0 50px 0;transition:all .3s ease !important}.login-page .login-container .login-form .cbi-button-apply:hover,.login-page .login-container .login-form .cbi-button-apply:focus{opacity:.9}.login-page .login-container footer{position:absolute;bottom:0;z-index:10;display:flex;justify-content:space-evenly;width:100%;margin-top:auto;padding:0 0 30px;color:var(--gray);text-align:center;line-height:1.6rem;font-size:.75rem}.login-page .login-container footer .ftc{position:absolute;bottom:30px;width:100%}.login-page .login-container footer .luci-link{display:block}.main{position:relative;width:100%;height:100%;overflow-y:auto;display:flex;flex-direction:row}.main-left{width:15rem;height:100%;flex-shrink:0;z-index:100;overflow-x:auto;word-break:break-word;background-color:var(--menu-bg-color);box-shadow:rgba(0,0,0,0.75) 0 0 15px -5px;transition:all .2s}.main-left .sidenav-header{padding:1.5rem .5rem;text-align:center}.main-left .sidenav-header .brand{display:block;margin:0 2rem;font-size:1.8rem;font-family:"TypoGraphica";color:var(--primary);text-decoration:none;text-align:center;cursor:default}.main-left .sidenav-header .brand .logo{max-width:100%;height:auto}.main-left .nav{margin-top:.5rem}.main-left .nav>li>a:first-child{display:block;position:relative;margin:.1rem .5rem;padding:.675rem 0 .675rem 2.5rem;font-size:1rem;text-decoration:none;border-radius:.25rem;cursor:default;transition:all .2s}.main-left .nav>li>a:first-child.active{color:#fff;background:var(--primary)}.main-left .nav>li>a:first-child.active::before{color:#fff !important}.main-left .nav>li>a:first-child.active::after{transform:rotate(90deg);color:#fff !important}.main-left .nav>li>a:first-child:hover{cursor:pointer;color:#fff;background:var(--primary)}.main-left .nav>li>a:first-child:hover::before{color:#fff !important}.main-left .nav>li>a:first-child::before{position:absolute;left:.8rem;padding-top:3px;font-family:'argon' !important;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\e915";color:var(--primary);transition:all .3s}.main-left .nav>.slide>.menu::before{transition:transform .1s ease-in-out}.main-left .nav>.slide>.menu.active::before{transition:transform .2s ease-in-out}.main-left .nav li{padding:0;cursor:pointer;user-select:none}.main-left .nav li a{display:block;color:var(--menu-color)}.main-left .nav li.slide{padding:0}.main-left .nav li.slide ul{display:none;overflow:hidden}.main-left .nav li.slide:hover{background:none}.main-left .nav li.slide .slide-menu{margin:0 .5rem 0 2.5rem;padding:0 .5rem}.main-left .nav li.slide .slide-menu.active{display:block}.main-left .nav li.slide .slide-menu li{position:relative;margin:0;border-radius:.25rem;background:none;list-style:none}.main-left .nav li.slide .slide-menu li a{padding:.5rem 0;text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.main-left .nav li.slide .slide-menu li::after{position:absolute;left:0;bottom:0;width:0;height:2px;content:"";background-color:var(--primary);transition:all .2s}.main-left .nav li.slide .slide-menu li:hover{background:none}.main-left .nav li.slide .slide-menu li:hover::after{width:100%}.main-left .nav li.slide .slide-menu .active{background:none;color:var(--menu-color)}.main-left .nav li.slide .slide-menu .active a{color:var(--menu-color)}.main-left .nav li.slide .slide-menu .active::after{position:absolute;left:0;bottom:0;width:100%;height:2px;content:"";background-color:var(--primary);transition:all .2s}.main-left .nav li.slide .slide-menu .active:hover{background:none}.main-left .nav li.slide .slide-menu .active:hover::after{width:100%}.main-left .nav li .menu{display:block;position:relative;margin:.1rem .5rem;padding:.675rem 0 .675rem 2.5rem;font-size:1rem;text-decoration:none;border-radius:.25rem;cursor:default;transition:all .2s}.main-left .nav li .menu.active{color:#fff;background:var(--primary)}.main-left .nav li .menu.active::before{color:#fff !important}.main-left .nav li .menu.active::after{transform:rotate(90deg);color:#fff !important}.main-left .nav li .menu:hover{cursor:pointer;color:#fff;background:var(--primary)}.main-left .nav li .menu:hover::before{color:#fff !important}.main-left .nav li .menu::before{position:absolute;left:.8rem;padding-top:3px;font-family:'argon' !important;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\e915";color:var(--primary);transition:all .3s}.main-left .nav li .menu::after{position:absolute;right:.5rem;top:.8rem;font-family:'argon' !important;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:'\e90f';color:#ced4da;text-rendering:auto;transition:all .3s}.main-left .nav li .menu[data-title="Status"]:before{content:"\e906";color:var(--primary)}.main-left .nav li .menu[data-title="System"]:before{content:"\e90a";color:#fb6340}.main-left .nav li .menu[data-title="Services"]:before{content:"\e909";color:#11cdef}.main-left .nav li .menu[data-title="NAS"]:before{content:"\e90c";color:#f3a4b5}.main-left .nav li .menu[data-title="VPN"]:before{content:"\e90b";color:#8965e0}.main-left .nav li .menu[data-title="Network"]:before{content:"\e908";color:#8965e0}.main-left .nav li .menu[data-title="Bandwidth_Monitor"]:before{content:"\e90d";color:#2dce89}.main-left .nav li .menu[data-title="Docker"]:before{content:"\e911";color:#6699ff}.main-left .nav li .menu[data-title="Statistics"]:before{content:"\e913";color:#8965e0}.main-left .nav li .menu[data-title="Control"]:before{content:"\e912";color:var(--primary)}.main-left .nav li .menu[data-title="Asterisk"]:before{content:"\e914";color:#fb6340}.main-left .nav li a[data-title="Log_out"]::before,.main-left .nav li .food[data-title="Log_out"]::before{content:"\e907";color:#adb5bd}.main-left[style*="overflow: hidden"]>.nav>.slide>.menu::before{display:none}.main-left::-webkit-scrollbar{width:5px;height:1px}.main-left::-webkit-scrollbar-thumb{background-color:#f6f9fc}.main-left::-webkit-scrollbar-track{background-color:#fff}.main-right{height:100%;flex-grow:1;overflow-x:hidden;overflow-y:auto;display:flex;flex-direction:column;transition:all .2s}.main-right>#maincontent{position:relative;z-index:50;flex:1;display:flex;flex-direction:column}.main-right>#maincontent>.container{margin:0 1.25rem 1rem 1.25rem;flex-grow:1;display:flex;flex-direction:column;gap:1rem}.main-right>#maincontent .Dashboard{color:var(--gray-dark) !important}.main-right>#maincontent .Dashboard h3{color:var(--gray-dark)}.main-right>#maincontent .Dashboard p{margin-top:3px;margin-bottom:3px}.main-right>#maincontent .Dashboard hr{border-top:1px solid #000}.main-right>#maincontent .Dashboard .dashboard-bg{background-color:#fff}.main-right>#maincontent .Dashboard .settings-info{padding-top:1em;padding-bottom:1em}.main-right>#maincontent .Dashboard .settings-info p span:nth-child(2){max-height:18.5px;top:4px}.main-right>#maincontent .Dashboard .settings-info .label{font-size:.7rem;padding:.2rem .6rem}header{position:relative;width:100%;padding:0;color:var(--header-color)}header.bg-primary{background-color:var(--primary) !important}header::after{content:"";position:absolute;width:100%;height:2rem;background-color:var(--primary) !important}header .fill{padding:.8rem 0;display:flex;border-bottom:0 solid rgba(255,255,255,0.08) !important}header .fill .container{width:100%;height:2rem;padding:0 1.25rem;display:flex;align-items:center}header .fill .container .flex1{flex:1}header .fill .container .flex1 .showSide{display:none;font-size:1.4rem;color:#fff}header .fill .container .flex1 .showSide:hover{text-decoration:none}header .fill .container .flex1 .brand{display:none;padding-left:1rem;font-size:1.5rem;font-family:"TypoGraphica";color:#fff;text-decoration:none;cursor:default;vertical-align:text-bottom}header .fill .container .pull-right{margin-top:0rem;float:right;display:flex}header .fill .status span{display:inline-block;margin:0 .25rem;padding:.3rem .8rem;font-size:.875rem;font-weight:bold;white-space:nowrap;text-decoration:none;text-transform:uppercase;text-shadow:none;border-radius:4px;cursor:pointer;transition:all .3s;box-shadow:0 2px 2px 0 rgba(0,0,0,0.16),0 0 2px 0 rgba(0,0,0,0.12)}header .fill .status span:last-child{margin-right:0}header .fill .status span[data-indicator="poll-status"]{color:#fff}header .fill .status span[data-style="active"]{background-color:var(--green)}header .fill .status span[data-style="inactive"]{color:#ffffff !important;background-color:#32325d}footer{padding:1rem;font-size:.875rem;color:#aaa;text-align:right;white-space:nowrap;overflow:hidden}footer>a{color:#aaa;text-decoration:none}#view{border-radius:.25rem;display:flex;flex-direction:column;gap:1rem}#view>.spinning{position:fixed;top:50%;left:50%;transform:translateX(-50%) translateY(-50%);padding:1rem;border-radius:.5rem;background:#ffffff;box-shadow:0 0 1rem 0 rgba(136,152,170,0.15)}#view>div:first-child{display:flex;flex-direction:column;gap:.875rem}#xhr_poll_status{display:flex;margin-left:.5rem}#xhr_poll_status *{color:#fff}.danger{background-color:var(--danger) !important}.warning{background-color:var(--warning) !important}.success{background-color:var(--success) !important}.notice{background-color:var(--info) !important;color:#fff}.error{color:var(--danger)}.alert,.alert-message{position:fixed;width:20rem;z-index:9000;left:50%;top:50%;transform:translate(-50%, -50%);font-weight:bold;padding:1rem 1.25rem;border:0;border-radius:.25rem !important;background-color:#fff;box-shadow:0 2px 2px 0 rgba(0,0,0,0.16),0 0 2px 0 rgba(0,0,0,0.12);text-shadow:none}.alert.error,.alert-message.error{background-color:var(--warning)}.alert h4,.alert-message h4{padding:.25rem 0;border-radius:.25rem}.alert .btn,.alert-message .btn{height:auto}.modal.alert-message{position:static;width:90%;min-width:270px;max-width:600px;left:auto;top:auto;transform:none;margin:5em auto}.alert-message>h4{font-size:110%;font-weight:bold}.alert-message>*{margin:.5rem 0}.alert-message .btn{padding:.3rem .6rem}.container .alert,.container .alert-message{margin-left:0;margin-right:0;margin-top:0rem;position:relative;top:0;transform:translate(-50%, 0);width:100%}#mwan3-service-status>.alert-message{left:.5rem;transform:none}.lg{margin:0;padding:0 !important}.cbi-section,.cbi-section-error,#iptables,.Firewall form,#cbi-network>.cbi-section-node,#cbi-wireless>.cbi-section-node,#cbi-wireless>#wifi_assoclist_table,[data-tab-title],[data-page^="admin-system-admin"]:not(.node-main-login) .cbi-map:not(#cbi-dropbear),[data-page="admin-system-opkg"] #maincontent>.container{font-family:inherit;font-weight:normal;font-style:normal;line-height:normal;min-width:inherit;padding:0;border:0;border-radius:.25rem;background-color:#fff;box-shadow:0 0 1rem 0 rgba(136,152,170,0.15)}.cbi-section:last-child,.cbi-section-error:last-child,#iptables:last-child,.Firewall form:last-child,#cbi-network>.cbi-section-node:last-child,#cbi-wireless>.cbi-section-node:last-child,#cbi-wireless>#wifi_assoclist_table:last-child,[data-tab-title]:last-child,[data-page^="admin-system-admin"]:not(.node-main-login) .cbi-map:not(#cbi-dropbear):last-child,[data-page="admin-system-opkg"] #maincontent>.container:last-child{margin:0;border:0}.cbi-modal .cbi-section,.cbi-section .cbi-section{padding:0;box-shadow:none}.cbi-modal .cbi-tabmenu{margin-left:0}.cbi-map{display:flex;flex-direction:column;gap:1rem}.cbi-map>.cbi-tabmenu+div{margin-top:-0.4375rem}.cbi-map-descr{font-size:small;line-height:1.5;padding:0 1.25rem}.cbi-section>.cbi-section-descr{padding-top:1rem !important;padding-bottom:1rem !important}.cbi-section>.cbi-section-descr:empty{display:none}.cbi-section-descr:not(:empty){font-size:small;line-height:1.5;padding:0rem 1rem}.cbi-section-descr:empty{display:none !important}.cbi-map-descr+fieldset{margin-top:1rem}.cbi-map-descr>abbr{cursor:help;text-decoration:underline}.cbi-section>legend{display:none !important}fieldset>fieldset,.cbi-section>.cbi-section{margin:0;padding:0;border:0;box-shadow:none}.cbi-section>h3:first-child,.panel-title{font-size:1.1rem;line-height:1;display:block;width:100%;margin:0;margin-bottom:0;padding:.8755rem 1.25rem;color:#32325d;color:var(--gray-dark)}.cbi-section>h3:first-child,.cbi-section>h4:first-child,.cbi-section>p:first-child,[data-tab-title]>h3:first-child,[data-tab-title]>h4:first-child,[data-tab-title]>p:first-child{padding:1rem 1.25rem}.cbi-section p{padding:1rem}.cbi-tblsection{overflow-x:auto}table{border-spacing:0;border-collapse:collapse}table,.table{overflow-y:hidden;width:100%;font-size:.75rem}.table .table-titles th{background-color:var(--lighter)}table>tbody>tr>td,table>tbody>tr>th,table>tfoot>tr>td,table>tfoot>tr>th,table>thead>tr>td,table>thead>tr>th,.table>.tbody>.tr>.td,.table>.tbody>.tr>.th,.table>.tfoot>.tr>.td,.table>.tfoot>.tr>.th,.table>.thead>.tr>.td,.table>.thead>.tr>.th,.table>.tr>.td.cbi-value-field,.table>.tr>.th.cbi-section-table-cell{padding:.5rem}.container>.cbi-section:first-of-type>.table[width="100%"]>.tr>.td{padding:.6rem}.cbi-section-table-cell{line-height:1.1;align-self:flex-end;flex:1 1 auto}tr>td,tr>th,.tr>.td,.tr>.th,.cbi-section-table-row::before,#cbi-wireless>#wifi_assoclist_table>.tr:nth-child(2){border-top:thin solid #ddd;padding:1.1em 1.25rem}#cbi-wireless .td,.table[width="100%"]>.tr:first-child>.td,[data-page="admin-network-diagnostics"] .tr>.td,.tr.table-titles>.th,.tr.cbi-section-table-titles>.th{border-top:0 !important;background-color:#f6f9fc;padding:1.1em 1.25rem;line-height:1.3rem}#cbi-network .tr:first-child>.td{border-top:0}.table[width="100%"]>.tr:first-child>.td{margin:auto 0}.cbi-section-table-row{margin-bottom:1rem;text-align:center !important;background:#f4f4f4}.cbi-section-table-row:last-child{margin-bottom:0}.cbi-section-table-row>.cbi-value-field .cbi-dropdown,.cbi-section-table-row>.cbi-value-field .cbi-input-select,.cbi-section-table-row>.cbi-value-field .cbi-input-text,.cbi-section-table-row>.cbi-value-field .cbi-input-password{width:100%}.cbi-section-table-row>.cbi-value-field .cbi-input-text,.cbi-section-table-row>.cbi-value-field .cbi-input-password{min-width:80px}.cbi-section-table-row>.cbi-value-field [data-dynlist]>input,.cbi-section-table-row>.cbi-value-field input.cbi-input-password{width:calc(100% - 1.5rem)}.cbi-section-table-row .td{text-align:center !important}.cbi-section-table-row .td .cbi-checkbox input[type="checkbox"]{margin:0}.control-group{display:inline-flex;width:100%;flex-wrap:wrap;gap:1rem}.control-group input{border-right-width:0;margin-right:0}.control-group input+button{border-bottom-left-radius:0;border-top-left-radius:0;margin-left:0;border-left-width:0}.control-group:has(> input:first-child + .cbi-button){gap:0 !important}.control-group:has(> input:first-child + .cbi-button) input{border-top-right-radius:0;border-bottom-right-radius:0;width:15.5rem;min-width:15.5rem}.control-group>*{vertical-align:middle}div>table>tbody>tr:nth-of-type(2n),div>.table>.tr:nth-of-type(2n){background-color:#f9f9f9}table table,.table .table,.cbi-value-field table,.cbi-value-field .table,td>table>tbody>tr>td,.td>.table>.tbody>.tr>.td,.cbi-value-field>table>tbody>tr>td,.cbi-value-field>.table>.tbody>.tr>.td{border:0}.btn,.cbi-button,.item::after{font-size:.875rem;display:inline-block;width:auto !important;padding:.5rem .75rem;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;transition:all .2s ease-in-out;text-align:center;vertical-align:middle;white-space:nowrap;text-decoration:none;border:0;border-radius:.25rem;background-color:#f0f0f0;background-image:none;appearance:none;-ms-touch-action:manipulation;touch-action:manipulation}.btn:not(button) ul:not(.dropdown) li{padding:0}.cbi-button-up,.cbi-button-down{display:inline-block;min-width:0;padding:.2rem 1rem;font-size:0;color:transparent !important;background:url(../icon/arrow.svg) no-repeat center;background-size:12px 20px}.cbi-button-up{transform:scaleY(-1)}.cbi-button:not(select){appearance:none !important}.btn:hover,.btn:focus,.btn:active,.cbi-button:hover,.cbi-button:focus,.cbi-button:active,.item:hover::after,.item:focus::after,.item:active::after,.cbi-page-actions .cbi-button-apply+.cbi-button-save:hover,.cbi-page-actions .cbi-button-apply+.cbi-button-save:focus,.cbi-page-actions .cbi-button-apply+.cbi-button-save:active{text-decoration:none;outline:0}.btn:hover,.btn:focus,.cbi-button:hover,.cbi-button:focus,.item:hover::after,.item:focus::after{box-shadow:0 3px 9px rgba(50,50,9,0),3px 4px 8px rgba(94,114,228,0.1)}.btn:active,.cbi-button:active,.item:active::after{box-shadow:0 3px 9px rgba(50,50,9,0),3px 4px 8px rgba(94,114,228,0.1)}.cbi-button-up:hover,.cbi-button-up:focus{box-shadow:0 3px 9px rgba(50,50,9,0),3px 4px 8px rgba(94,114,228,0.1)}.cbi-button-up:active{box-shadow:0 3px 9px rgba(50,50,9,0),3px 4px 8px rgba(94,114,228,0.1)}.btn:disabled,.cbi-button:disabled{cursor:not-allowed;pointer-events:none;opacity:.5;box-shadow:none}.alert-message [class="btn"],.modal div[class="btn"],.cbi-button-find,.cbi-button-link,.cbi-button-up,.cbi-button-down,.cbi-button-neutral,.cbi-button[name="zero"],.cbi-button[name="restart"],.cbi-button[onclick="hide_empty(this)"]{color:#fff;border:thin solid #8898aa;background-color:#8898aa}.btn.primary,.cbi-page-actions .cbi-button-save,.cbi-page-actions .cbi-button-apply+.cbi-button-save,.cbi-button-add,.cbi-button-save,.cbi-button-positive,.cbi-button-link,.cbi-button[value="Enable"],.cbi-button[value="Scan"],.cbi-button[value^="Back"],.cbi-button-neutral[onclick="handleConfig(event)"]{font-weight:normal;color:#fff !important;border:thin solid #5e72e4;border:thin solid var(--primary);background-color:#5e72e4;background-color:var(--primary)}.cbi-page-actions .cbi-button-apply,.cbi-section-actions .cbi-button-edit,.cbi-button-edit,.cbi-button-apply,.cbi-button-reload,.cbi-button-action,.cbi-button[value="Submit"],.cbi-button[value="Upload"],.cbi-button[value$="Apply"],.cbi-button[onclick="addKey(event)"]{font-weight:normal;color:#fff !important;border:thin solid #5e72e4;border:thin solid var(--primary);background-color:#5e72e4;background-color:var(--primary)}.btn.danger,.cbi-section-remove>.cbi-button,.cbi-button-remove,.cbi-button-reset,.cbi-button-negative,.cbi-button[value="Stop"],.cbi-button[value="Kill"],.cbi-button[onclick="reboot(this)"],.cbi-button-neutral[value="Restart"]{font-weight:normal;color:#fff;border:thin solid #f5365c;border:thin solid var(--red);background-color:#f5365c;background-color:var(--red)}.btn[value="Dismiss"],.cbi-button[value="Terminate"],.cbi-button[value="Reset"],.cbi-button[value="Disabled"],.cbi-button[onclick^="iface_reconnect"],.cbi-button[onclick="handleReset(event)"],.cbi-button-neutral[value="Disable"]{font-weight:normal;color:var(--white);border:thin solid #eea236;background-color:#f0ad4e}.cbi-button-success,.cbi-button-download{font-weight:normal;color:var(--white);border:thin solid #4cae4c;background-color:#5cb85c}.cbi-page-actions .cbi-button-link:first-child{float:left}.cbi-page-actions .cbi-dropdown .open{color:var(--white)}.cbi-page-actions .cbi-dropdown .more{display:block;width:1px;height:100%;overflow:hidden;text-indent:5000px;background-color:var(--white)}.a-to-btn{text-decoration:none}.cbi-value-field .cbi-button-add{font-weight:bold;padding:1px 6px;display:inline-block;align-items:center}.tabs{padding:0 1rem;background-color:#FFFFFF;border-radius:.25rem;box-shadow:0 4px 8px rgba(0,0,0,0.03);white-space:nowrap;overflow-x:auto}.tabs::-webkit-scrollbar{width:1px;height:5px}.tabs::-webkit-scrollbar-thumb{background-color:#f6f9fc}.tabs::-webkit-scrollbar-track{background-color:#fff}.tabs li[class~="active"],.tabs li:hover{cursor:pointer;border-bottom:.18751rem solid #5e72e4;border-bottom:.18751rem solid var(--primary);color:#5e72e4;color:var(--primary);background-color:#e4e9ee;margin-bottom:0;border-radius:0}.tabs li[class~="active"] a,.tabs li:hover a{color:#5e72e4;color:var(--primary)}.tabs li{font-size:.875rem;display:inline-block;padding:.875rem 0;border-bottom:.18751rem solid rgba(0,0,0,0);margin:0;transition:all .2s}.tabs li a{text-decoration:none;color:#404040;padding:.5rem .8rem}.tabs li:hover{border-bottom:.18751rem solid #5e72e4;border-bottom:.18751rem solid var(--primary)}.cbi-tabmenu{color:white;padding:.5rem 1rem 0 1rem;white-space:nowrap;overflow-x:auto}.cbi-tabmenu::-webkit-scrollbar{width:1px;height:5px}.cbi-tabmenu::-webkit-scrollbar-thumb{background-color:#f6f9fc}.cbi-tabmenu::-webkit-scrollbar-track{background-color:#fff}.cbi-tabmenu li{background:#dce3e9;display:inline-block;font-size:.875rem;border-top-left-radius:.25rem;border-top-right-radius:.25rem;padding:.5rem 0;border-bottom:.18751rem solid rgba(0,0,0,0);margin:0 .2rem;position:relative}.cbi-tabmenu li a{text-decoration:none;color:#404040;padding:.5rem .8rem}.cbi-tabmenu li:hover{cursor:pointer;border-bottom:.18751rem solid #5e72e4;border-bottom:.18751rem solid var(--primary);color:#5e72e4;color:var(--primary);background-color:#e4e9ee;margin-bottom:0}.cbi-tabmenu li:hover a{color:#525f7f}.cbi-tabmenu li[class~="cbi-tab"]{border-bottom:.18751rem solid #5e72e4;border-bottom:.18751rem solid var(--primary);color:#5e72e4;color:var(--primary);background-color:#e4e9ee;margin-bottom:0}.cbi-tabmenu li[class~="cbi-tab"] a{color:#5e72e4;color:var(--primary)}.cbi-tab-descr{padding:0 1.25rem}.cbi-section-node{display:flex;flex-direction:column;gap:.875rem}.cbi-section-node>[id^="cbi-config-"]{display:flex;flex-direction:column;gap:.875rem}.cbi-section-node>[id^="cbi-config-"]:first-child{padding-top:1rem}.cbi-section-node>[id^="cbi-config-"]:last-child{padding-bottom:1rem}.cbi-section .cbi-section-remove:nth-of-type(2n),.container>.cbi-section .cbi-section-node:nth-of-type(2n){background-color:#f9f9f9}[data-tab-title]{overflow:hidden;height:0;opacity:0;margin:0;padding:1rem 0;display:none !important}[data-tab-title] p{margin-left:1rem;margin-bottom:1rem}[data-tab-active="true"]{overflow:visible;height:auto;opacity:1;display:inherit !important;transition:opacity .25s ease-in;margin:inherit !important}.cbi-section[id] .cbi-section-remove:nth-of-type(4n+3),.cbi-section[id] .cbi-section-node:nth-of-type(4n+4){background-color:#f9f9f9}.cbi-section-node-tabbed{margin-top:0;border:0 solid #d4d4d4;border-radius:.25rem}.cbi-section-node-tabbed>div{display:flex;flex-direction:column;gap:.875rem}.cbi-tabcontainer>.cbi-value:nth-of-type(2n){background-color:#f9f9f9}.cbi-value-field{display:table-cell}.cbi-value-field div+br{display:none}.cbi-value-description{line-height:1.25;display:table-cell;padding:.5rem 0;opacity:.5;font-size:.75rem}.cbi-value-description abbr{color:#32325d;color:var(--gray-dark)}.cbi-value-description:empty{display:none}.cbi-value-title{display:table-cell;float:left;width:23rem;padding-right:2rem;text-align:right;word-wrap:break-word}.cbi-value{display:inline-block;width:100%;padding:0 1rem;line-height:2.4rem}.cbi-value ul{line-height:1.25}.cbi-value-field.cbi-dropdown-open .cbi-dropdown{border-color:var(--primary);box-shadow:0 3px 9px rgba(50,50,9,0),3px 4px 8px rgba(94,114,228,0.1)}.cbi-value-field .cbi-dropdown,.cbi-value-field .cbi-input-select,.cbi-value input[type="text"],.cbi-value input[type="password"],.cbi-value textarea{min-width:18rem}#cbi-firewall-zone .cbi-input-select,#cbi-network-switch_vlan .cbi-input-select{min-width:11rem}#cbi-network-switch_vlan .cbi-input-text{max-width:3rem}.cbi-input-file{appearance:auto !important}.cbi-input-file::-webkit-file-upload-button{display:inline-block !important}.cbi-input-invalid{color:#f5365c !important;border-color:#f5365c !important}.cbi-section-error{font-weight:bold;line-height:1.42857143;margin:18px;padding:6px;border:thin solid #f5365c;border-radius:3px;background-color:#fce6e6}.cbi-section-error ul{margin:0 0 0 20px}.cbi-section-error ul li{font-weight:bold;color:#f5365c}.td[data-title]::before{font-weight:bold;display:none;padding:.25rem 0;content:attr(data-title) ":\20";text-align:left;white-space:nowrap}.tr.placeholder .td[data-title]::before{display:none}.tr[data-title]::before,.tr.cbi-section-table-titles.named::before{font-weight:bold;display:table-cell;align-self:center;flex:1 1 5%;padding:.25rem;content:attr(data-title) "\20";text-align:center;vertical-align:middle;white-space:normal;word-wrap:break-word;background-color:#f6f9fc}.cbi-rowstyle-1{background-color:#f9f9f9}.cbi-rowstyle-2{background-color:#eee}.cbi-rowstyle-2 .cbi-button-up,.cbi-rowstyle-2 .cbi-button-down,body:not(.Interfaces) .cbi-rowstyle-2:first-child{background-color:#fff !important}.cbi-section-table .cbi-section-table-titles .cbi-section-table-cell{width:auto !important}.td.cbi-section-actions{text-align:right !important;vertical-align:middle}.td.cbi-section-actions>*{display:inline-flex;gap:1rem}.td.cbi-section-actions>*>*,.td.cbi-section-actions>*>form>*{display:flex;align-items:center}.td.cbi-section-actions>*>form{display:inline-flex;margin:0}.td .cbi-checkbox{justify-content:center}.cbi-checkbox{display:flex;align-items:center;height:2.5rem}.cbi-dynlist{line-height:1.3;flex-direction:column;min-height:30px;cursor:text;gap:.875rem}.cbi-dynlist>.item{display:inline-flex;flex-wrap:nowrap;position:relative;max-width:25rem;pointer-events:none;color:#8898aa;outline:0}.cbi-dynlist>.item::after{content:"\00D7";pointer-events:auto;display:flex;align-items:center;justify-content:center;width:2.5rem !important;height:2.5rem;margin:0;font-weight:normal;font-size:1rem;line-height:1.5rem;color:#fff;border:1px solid #f5365c;border-radius:0 .25rem .25rem 0;outline:0;background-color:var(--red);background-image:none;box-shadow:none;box-sizing:border-box}.cbi-dynlist>.item>span{display:block;padding:.5rem .75rem;min-width:15.5rem;width:15.5rem;height:2.5rem;overflow:hidden;text-overflow:ellipsis;user-select:text;white-space:nowrap;word-break:break-word;line-height:1.5em;color:#8898aa;border:1px solid #dee2e6;border-radius:.25rem 0 0 .25rem;outline:0;background-image:none;box-shadow:none;box-sizing:border-box;transition:box-shadow .15s ease}.cbi-dynlist>.add-item{display:inline-flex;align-items:center;width:100%;min-width:15.5rem;flex-wrap:nowrap}.cbi-dynlist>.add-item input{display:block;padding:.5rem .75rem;box-sizing:border-box;min-width:15.5rem;width:15.5rem;transition:box-shadow .15s ease;white-space:nowrap;word-break:break-word;font-size:.875rem;line-height:1.5rem;color:#8898aa;border:1px solid #dee2e6;border-radius:.25rem 0 0 .25rem;border-right-width:0;outline:0;background-image:none;box-shadow:none}.cbi-dynlist>.add-item .cbi-button{display:flex;width:auto !important;padding-left:.8rem;padding-right:.8rem;margin-left:0;align-items:center;justify-content:center;font-size:.875rem;line-height:1.5rem;outline:0;background-image:none;background-color:var(--gray);box-shadow:none;color:var(--white);border-color:var(--gray);border-radius:.25rem;border-top-left-radius:0;border-bottom-left-radius:0}.cbi-dynlist>.add-item .cbi-button-add{width:2.5rem !important;padding:.5rem 0 !important;font-weight:normal;font-size:1.2rem;color:#fff;background-color:var(--primary);border:1px solid var(--primary)}.cbi-dynlist>.add-item:not([ondrop])>input{overflow:hidden;min-width:15.5rem;width:15.5rem;white-space:nowrap;text-overflow:ellipsis}.cbi-dynlist>.item.drag-over,.cbi-dynlist>.add-item:has(.drag-over){border-top:1px solid var(--red);margin-top:-1px}.cbi-dynlist[name="sshkeys"]>.item{max-width:none}.cbi-dynlist>.cbi-dynlist>.add-item[ondrop]>input{min-width:13rem}.cbi-dynlist,.cbi-dropdown{position:relative;display:inline-flex;padding-right:.25rem;min-height:2.1875rem}.cbi-dropdown{align-items:center}.cbi-dropdown.btn>ul:not(.dropdown),.cbi-dropdown.cbi-button>ul:not(.dropdown){margin:0 .75rem}.cbi-dropdown[placeholder*="select"]{max-width:25rem;height:auto;margin-top:-3px}.cbi-dropdown>ul{display:flex;overflow-x:hidden;overflow-y:auto;width:100%;padding:0;list-style:none;outline:0}.cbi-dropdown>ul.preview{display:none}.cbi-dropdown.cbi-button-apply,.cbi-dropdown.cbi-button-action{padding:0}.cbi-button-apply>ul.preview{display:none}.cbi-button-apply>ul.preview li{color:#fff}.cbi-button-apply>ul:first-child li{color:#fff}.cbi-dropdown>.open{display:flex;align-items:center;width:1rem;height:100%;padding:0 .75rem;cursor:pointer;user-select:none;font-size:0;color:#8898aa;background-color:currentColor;mask-image:var(--dropdown-arrow-icon);mask-repeat:no-repeat;mask-size:100% 1rem;mask-position:center;mask-mode:match-source}.cbi-dropdown>.more,.cbi-dropdown>ul>li[placeholder]{display:none}.cbi-dropdown>ul>li{display:none;overflow:hidden;align-items:center;align-self:center;flex-grow:1;flex-shrink:1;min-height:20px;padding:.125rem .25em;white-space:nowrap;text-overflow:ellipsis}.cbi-dropdown>ul>li .hide-open{display:initial}.cbi-dropdown>ul>li .hide-close{display:none}.cbi-dropdown>ul>li[display]:not([display="0"]){border-left:thin solid #ccc}.cbi-dropdown[empty]>ul{max-width:1px}.cbi-dropdown>ul>li>form{display:none;margin:0;padding:0;pointer-events:none}.cbi-dropdown>ul>li img{margin-right:.25em;vertical-align:middle}.cbi-dropdown>ul>li>form>input[type="checkbox"]{height:auto;margin:0}.cbi-dropdown>ul>li input[type="text"]{height:2rem;min-width:16rem;padding:0 .5rem}.cbi-dropdown[open]>ul.dropdown{position:absolute;z-index:1100;display:block;width:auto;min-width:100%;max-width:none;max-height:200px !important;border:0 solid #918e8c;background:#ffffff;box-shadow:0 0 4px #918e8c;border-radius:.25rem;color:var(--main-menu-color);margin-left:0 !important;margin-top:.25rem;left:0}.cbi-dropdown[open]>ul.dropdown li{color:#000}.cbi-dropdown>ul>li[display],.cbi-dropdown[open]>ul.preview,.cbi-dropdown[open]>ul.dropdown>li,.cbi-dropdown[multiple]>ul>li>label,.cbi-dropdown[multiple][open]>ul.dropdown>li,.cbi-dropdown[multiple][more]>.more,.cbi-dropdown[multiple][empty]>.more{display:flex;align-items:center;flex-grow:1}.cbi-dropdown[empty]>ul>li,.cbi-dropdown[optional][open]>ul.dropdown>li[placeholder],.cbi-dropdown[multiple][open]>ul.dropdown>li>form{display:block}.cbi-dropdown[open]>ul.dropdown>li .hide-open{display:none}.cbi-dropdown[open]>ul.dropdown>li .hide-close{display:initial}.cbi-dropdown[open]>ul.dropdown>li{border-bottom:thin solid #ccc;padding:.5rem .75rem;cursor:pointer}.cbi-dropdown[open]>ul.dropdown>li label{margin-left:.5rem}.cbi-dropdown[open]>ul.dropdown>li[selected]{background:#e4e9ee}.cbi-dropdown[open]>ul.dropdown>li.focus{background:#e4e9ee;outline:none}.cbi-dropdown[open]>ul.dropdown>li:last-child{margin-bottom:0;border-bottom:0}.cbi-dropdown[open]>ul.dropdown>li[unselectable]{opacity:.7}.cbi-dropdown[open]>ul.dropdown>li>input.create-item-input:first-child:last-child{width:100%}.cbi-dropdown[disabled]{pointer-events:none;opacity:.6}.cbi-dropdown .zonebadge{width:100%}.cbi-dropdown[open] .zonebadge{width:auto}.cbi-progressbar{position:relative;display:flex;width:100%;font-size:.75rem;background-color:#e9ecef;border-radius:.5rem;height:1rem;overflow:hidden}.cbi-progressbar>div{display:block;position:absolute;height:100%;background-color:var(--primary);border-radius:.5rem;transition:width .3s}.cbi-progressbar::after{content:attr(title);position:absolute;font-size:.75rem;color:var(--bs-heading-color);width:100%;height:100%;text-align:center;line-height:1rem;z-index:2}#modal_overlay{position:fixed;z-index:900;top:0;right:10000px;bottom:0;left:-10000px;overflow-y:scroll;transition:opacity .125s ease-in;opacity:0;background:rgba(0,0,0,0.7);-webkit-overflow-scrolling:touch}.modal{display:flex;align-items:center;flex-wrap:wrap;width:90%;min-width:270px;max-width:600px;min-height:32px;margin:5em auto;padding:1rem;border-radius:.25rem !important;background:#fff;box-shadow:0 2px 2px 0 rgba(0,0,0,0.16),0 0 2px 0 rgba(0,0,0,0.12)}.modal>*{line-height:normal;flex-basis:100%;margin-bottom:.5em;max-width:100%}.modal>pre,.modal>textarea{font-size:1rem;font-size-adjust:.35;overflow:auto;padding:.5rem;cursor:auto;white-space:pre-wrap;color:var(--dark-primary);outline:0;border-radius:.25rem;border:1px solid var(--lighter);transition:all .2s ease}.modal>pre:focus,.modal>textarea:focus{border:1px solid var(--primary);box-shadow:0 3px 9px rgba(50,50,9,0),3px 4px 8px rgba(94,114,228,0.1)}.modal>h4{display:block;flex-grow:1;max-width:none;padding:1rem;margin:-1rem -1rem 0 -1rem;font-size:1rem;box-shadow:0 4px 8px rgba(0,0,0,0.03);border-radius:.25rem 0 0 .25rem}.modal h5{margin-top:1rem;font-weight:600}.modal label>input[type="checkbox"]{top:0}.modal ul li{list-style-type:square;color:#808080}.modal p{word-break:break-word;margin-top:1rem}.modal .label{font-size:.6rem;font-weight:normal;padding:.1rem .3rem;padding-bottom:0;cursor:default;border-radius:0}.modal .label.warning{background-color:#f0ad4e !important}.modal .btn{padding:.45rem .8rem}.modal.cbi-modal{max-width:90%;max-height:none}.modal .cbi-map{margin-bottom:1rem}body.modal-overlay-active{overflow:hidden;height:100vh}body.modal-overlay-active #modal_overlay{right:0;left:0;opacity:1}.spinning{position:relative;padding-left:32px !important}.spinning::before{position:absolute;top:0;bottom:0;left:.2em;width:32px;content:"";background:url(/luci-static/resources/icons/loading.svg) no-repeat center;background-size:16px}.hidden{display:none}.left,.left::before{text-align:left !important}.right,.right::before{text-align:right !important}.button-row{display:flex;flex-direction:row;gap:1rem;align-items:center;justify-content:flex-end;margin-bottom:0 !important}.center,.center::before{text-align:center !important}.top{align-self:flex-start !important;vertical-align:top !important}.bottom{align-self:flex-end !important;vertical-align:bottom !important}.inline{display:inline}.cbi-page-actions{padding:1rem;justify-content:flex-end;display:flex;gap:1rem}.cbi-page-actions>form[method="post"]{display:inline-block}.th[data-type="button"],.td[data-type="button"],.th[data-type="fvalue"],.td[data-type="fvalue"]{flex:1 1 2em;text-align:center}.ifacebadge{display:inline-flex;align-items:center;gap:.2rem;padding:.25rem .8rem;background:#eee;border-radius:4px}td>.ifacebadge,.td>.ifacebadge{font-size:.875rem;background-color:#f0f0f0}.ifacebadge>em,.ifacebadge>img{display:inline-block;margin:0 .75rem}.ifacebadge>img+img{margin:0 .2rem 0 0}.network-status-table{display:flex;flex-wrap:wrap}.network-status-table .ifacebox{flex-grow:1;border-radius:.25rem;overflow:hidden;margin:1rem}.network-status-table .ifacebox-body{display:flex;flex-direction:column;height:100%;gap:.5em}.network-status-table .ifacebox-body>span{flex:10 10 auto}.network-status-table .ifacebox-body>div{display:flex;flex-wrap:wrap;gap:.5rem;height:100%}.network-status-table .ifacebox-body .ifacebadge{align-items:center;flex:1 1 auto;min-width:220px;padding:.5em;background-color:#fff}.network-status-table .ifacebox-body .ifacebadge>span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.cbi-input-textarea{font-family:inherit;width:100%;font-size:.875rem;min-height:14rem;padding:.8rem;color:#8898aa;border-radius:.25rem;border:1px solid #dee2e6;min-width:16rem}#content_syslog{box-shadow:0 4px 8px rgba(0,0,0,0.03)}#syslog{font-size:small;font-family:'Google Sans';line-height:1.25;overflow-y:hidden;width:100%;min-height:15rem;padding:1rem;resize:none;color:#242424;border:0;border-radius:.25rem;background-color:#ffffff}#syslog:focus{outline:0}#cbi-ddns #syslog{overflow:auto}.uci-change-list{font-family:inherit;overflow:scroll;width:100%;display:flex;flex-direction:column;flex-wrap:wrap}.uci-change-list ins,.uci-change-legend-label ins{display:block;padding:2px;text-decoration:none;border:thin solid #0f0;background-color:#cfc}.uci-change-list del,.uci-change-legend-label del{font-style:normal;display:block;padding:2px;text-decoration:none;border:thin solid #f00;background-color:#fcc}.uci-change-list var,.uci-change-legend-label var{font-style:normal;display:block;padding:2px;text-decoration:none;border:thin solid #ccc;background-color:#eee}.uci-change-list var ins,.uci-change-list var del{font-style:normal;padding:0;white-space:pre;border:0}.uci-change-legend{padding:5px}.uci-change-legend-label{float:left;width:150px}.uci-change-legend-label>ins,.uci-change-legend-label>del,.uci-change-legend-label>var{display:block;float:left;width:10px;height:10px;margin-right:4px}.uci-change-legend-label var ins,.uci-change-legend-label var del{line-height:.4;border:0}.uci-change-list var,.uci-change-list del,.uci-change-list ins{padding:.5rem}.uci-dialog .cbi-section{padding:.5rem}.uci-dialog .cbi-section .uci-change-legend{line-height:15px;padding:10px 20px 0 20px}.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label{padding:0;margin:0;position:relative;float:none;display:inline-block;width:25%}.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label>ins,.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label>del{width:14px;height:14px}.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label>var{position:relative;width:14px;height:14px}.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label>var ins,.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label>var del{position:absolute;left:2px;top:2px;right:2px;bottom:2px}.uci-dialog .cbi-section .uci-change-list{overflow:auto}.uci-dialog .cbi-section .uci-change-list+.right .btn{color:#333}.uci-dialog .cbi-section .uci-change-list+.right .cbi-dropdown ul:not(.dropdown) li{color:#fff}.uci-dialog .cbi-section .uci-change-list+.right .cbi-button{padding:.45rem .8rem}#iwsvg,#iwsvg2,#bwsvg{border:thin solid #d4d4d4 !important}#iwsvg{border-top:0 !important}.ifacebox{line-height:1.25;display:inline-flex;overflow:hidden;flex-direction:column;border-radius:4px;min-width:100px;background-color:#f9f9f9}.ifacebox-head{padding:.25em;background:#eee}.ifacebox-head.active{background:#5e72e4;background:var(--primary)}.ifacebox-head.active *{color:#fff;color:var(--white)}.ifacebox-body{padding:.875rem 1rem;line-height:1.6em}.cbi-image-button{margin-left:.5rem}.zonebadge{display:inline-block;padding:.2rem .5rem;border-radius:4px}.zonebadge .ifacebadge{margin:.1rem .2rem;padding:.2rem .3rem;border:thin solid #6c6c6c}.zonebadge>input[type="text"]{min-width:10rem;margin-top:.3rem;padding:.16rem 1rem}.zonebadge>em,.zonebadge>strong{display:inline-block;margin:0 .2rem}.cbi-value-field .cbi-input-checkbox,.cbi-value-field .cbi-input-radio{margin-top:.1rem}.cbi-value-field>ul>li{display:flex}.cbi-value-field>ul>li>label{margin-top:.5rem}.cbi-value-field>ul>li .ifacebadge{margin-top:-0.5rem;margin-left:.4rem;background-color:#eee}.cbi-section-table-row>.cbi-value-field .cbi-dropdown{min-width:3rem}.cbi-section-create{display:inline-flex;align-items:center;padding:.875rem 1rem}.cbi-section-create>div:first-child>input{width:100%;border-right:none;border-bottom-right-radius:0;border-top-right-radius:0}.cbi-section-create>div:first-child+button{border-top-left-radius:0;border-bottom-left-radius:0}.cbi-section-remove{padding:.5rem 1rem}div.cbi-value var,td.cbi-value-field var,.td.cbi-value-field var{font-style:italic;color:#0069d6}.cbi-optionals{padding:1rem 1rem 0 1rem;border-top:thin solid #ccc}.cbi-dropdown-container{position:relative}.cbi-tooltip-container,span[data-tooltip],span[data-tooltip] .label{cursor:help !important}.cbi-tooltip{position:absolute;z-index:1000;left:-10000px;box-shadow:0 3px 9px rgba(50,50,9,0),3px 4px 8px rgba(94,114,228,0.1);border-radius:.25rem;background:#fff;white-space:pre;padding:.5rem;opacity:0;transition:opacity .25s ease-in;transform:translate(-50%, 10%)}.cbi-tooltip-container:hover .cbi-tooltip:not(:empty){left:auto;transition:opacity .25s ease-in;opacity:1}.zonebadge .cbi-tooltip{margin:-1.5rem 0 0 -0.5rem;padding:.25rem;background:inherit}.zonebadge-empty{color:#404040;background:repeating-linear-gradient(45deg, rgba(204,204,204,0.5), rgba(204,204,204,0.5) 5px, rgba(255,255,255,0.5) 5px, rgba(255,255,255,0.5) 10px)}.zone-forwards{display:flex;min-width:10rem}.zone-forwards>*{flex:1 1 45%}.zone-forwards>span{flex-basis:10%;padding:0 .25rem;text-align:center}.zone-forwards .zone-src,.zone-forwards .zone-dest{display:flex;flex-direction:column}.label{font-size:.875rem;font-weight:bold;padding:.3rem .8rem;white-space:nowrap;text-decoration:none;text-transform:uppercase;color:#fff !important;border-radius:3px;background-color:#bfbfbf;text-shadow:none}label>input[type="checkbox"],label>input[type="radio"]{position:relative;top:.4rem;right:.2rem;margin:0;vertical-align:bottom}label[data-index][data-depends]{padding-right:2em}.showSide{display:none}.darkMask{position:fixed;z-index:99;display:none;width:100%;height:100%;content:"";top:0;background-color:rgba(0,0,0,0.56);transition:all .2s}.darkMask.active{display:block}#diag-rc-output>pre,#command-rc-output>pre,[data-page="admin-services-wol"] .notice code{font-size:1.2rem;font-size-adjust:.35;line-height:normal;display:block;overflow-y:hidden;width:100%;padding:8.5px;white-space:pre;color:#eee;background-color:#101010;box-shadow:0 2px 2px 0 rgba(0,0,0,0.16),0 0 2px 0 rgba(0,0,0,0.12)}input[name="ping"],input[name="traceroute"],input[name="nslookup"]{width:80%}.controls{gap:.5rem;margin:0 1.25rem !important}.controls>div{gap:.5rem}.controls>*>.btn:not([aria-label$="page"]){flex-grow:initial !important;margin-top:.25rem}.controls>#pager>.btn[aria-label$="page"]{font-size:1.4rem;font-weight:bold}.controls>*>label{margin-bottom:.2rem}.td.version,.td.size{white-space:normal !important;word-break:break-word}.cbi-tabmenu+.cbi-section{margin-top:0}.cbi-tab-disabled[data-errors]::after{position:absolute;top:-0.25rem;right:-0.25rem;content:attr(data-errors);background-color:var(--red);color:#fff;width:.875rem;height:.875rem;border-radius:.875rem;text-align:center;display:inline-flex;flex-direction:column;justify-content:center;font-size:.75em}.commands{display:grid !important;grid-template-columns:repeat(auto-fit, minmax(300px, 1fr));gap:.75rem}.commandbox{padding:.5rem 1rem;gap:.5rem;border-bottom:thin solid #ccc;border-radius:.25rem;background:#eee;box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.commandbox h3{line-height:normal !important;overflow:hidden;margin:6px 0 !important;white-space:nowrap;text-overflow:ellipsis}.commandbox div{left:auto !important}.commandbox code{overflow:hidden;max-width:fit-content;padding:2px 3px;white-space:nowrap;text-overflow:ellipsis}.commandbox code:hover{overflow-y:auto;max-height:50px;white-space:normal}.commandbox p:first-of-type{margin-top:-6px}.commandbox p:nth-of-type(2){margin-top:2px}#command-rc-output .alert-message{line-height:1.42857143;position:absolute;top:40px;right:32px;max-width:40%;margin:0;animation:anim-fade-in 1.5s forwards;word-break:break-word;opacity:0}@keyframes anim-fade-in{100%{opacity:1}}.fb-container .cbi-button{height:auto !important}#cbi-usb_printer-printer em{display:block;padding:1rem;text-align:center}pre.command-output{padding:1.5rem}[data-page="admin-system-autoreboot"] #cbi-autoreboot{margin-top:0}[data-page="admin-system-system"] .control-group{gap:.875rem}[data-page="admin-system-system"] .cbi-dynlist{margin:.25rem 0}[data-page="admin-system-admin"] .cbi-map h2,[data-page="admin-system-admin-password"] .cbi-map h2,[data-page="admin-system-admin"] .cbi-map .cbi-map-descr,[data-page="admin-system-admin-password"] .cbi-map .cbi-map-descr{margin-left:0;color:var(--gray-dark)}[data-page="admin-system-admin"] .cbi-section-node,[data-page="admin-system-admin-password"] .cbi-section-node{padding:1rem 0}[data-page="admin-system-admin-sshkeys"] .cbi-dynlist{margin-left:1rem}[data-page="admin-system-startup"] #view>div:first-child div[data-initialized="true"]{margin-top:-0.4375rem}[data-page="admin-system-startup"] [data-tab-title] p{margin-left:0;margin-bottom:0;display:flex;align-items:center;justify-content:center}[data-page="admin-system-startup"] textarea{line-height:1.25;overflow-y:auto;width:100%;min-height:15rem;margin:0 1rem;padding:1rem;resize:none;color:#8898aa;border-radius:.25rem;border:1px solid #dee2e6}[data-page="admin-system-startup"] textarea:focus-visible{outline:none;box-shadow:none;border:1px solid var(--primary)}[data-page="admin-system-crontab"] #view p textarea{line-height:1.25;overflow-y:hidden;width:100%;min-height:15rem;padding:1rem;resize:none;background-color:transparent;background:var(--white);outline:none;color:#8898aa;border-radius:.25rem;border:1px solid #dee2e6;transition:all .2s ease}[data-page="admin-system-crontab"] #view p textarea:focus-visible{outline:none;box-shadow:none;border:1px solid var(--primary)}[data-page="admin-system-crontabhelper"] .crontab-row .dropdown-container{display:flex;flex-direction:column;align-items:center;gap:.5rem}[data-page="admin-system-crontabhelper"] .crontab-row .dropdown-container div{display:flex;flex-direction:column;align-items:center;gap:.5rem}[data-page="admin-system-partexp-global"] .cbi-section{padding:1rem 0}[data-page="admin-system-attendedsysupgrade"] #view .cbi-button{margin-left:0 !important;margin-top:1rem !important}[data-page="admin-system-attendedsysupgrade-configuration"] .cbi-map .cbi-map-descr{padding-bottom:0}[data-page="admin-system-flash"] .cbi-value{padding:0 1rem}[data-page="admin-system-flash"] .cbi-section .cbi-section{margin-top:0}[data-page="admin-system-flash"] .cbi-map-tabbed{border-radius:.25rem}[data-page="admin-system-flash"] .cbi-section-node{padding-top:0;padding-bottom:.5rem}[data-page="admin-system-flash"] legend{font-size:1.2rem;width:100%;display:block;border-bottom:1px solid rgba(0,0,0,0.05);line-height:1.5;margin-bottom:0;letter-spacing:.1rem;color:#32325d;font-weight:bold;padding:1rem 0 1rem 1rem}[data-page="admin-system-flash"] .cbi-section-descr{font-weight:600;padding:1rem 0 1rem 1rem;color:#525f7f}[data-page="admin-system-flash"] .cbi-page-actions{padding:0rem 1rem 1rem 0rem}[data-page="admin-system-flash"] .modal label>input[type="checkbox"]{top:-0.25rem}[data-page="admin-system-flash"] .modal .btn{white-space:normal !important}[data-page="admin-system-flash"] .modal label>input[type="checkbox"]{vertical-align:text-top;top:auto}[data-page="admin-system-filetransfer"] #cbi-upload{margin-top:0}[data-page="admin-system-filetransfer"] .cbi-section-table{box-shadow:0 4px 8px rgba(0,0,0,0.03)}[data-page="admin-system-fileassistant"] #upload-toggle{display:none !important}[data-page="admin-system-fileassistant"] .fb-container .panel-title{padding:.5rem .75rem !important}[data-page="admin-system-fileassistant"] .cbi-section.fb-container{padding:.5rem}[data-page="admin-system-fileassistant"] .cbi-section.fb-container label.cbi-value-title{line-height:1.5rem;padding:.5rem .75rem;width:60px;text-align:left}[data-page="admin-system-fileassistant"] .cbi-section.fb-container div.cbi-value-field{width:100%;display:block;padding-left:60px}[data-page="admin-system-fileassistant"] .cbi-section.fb-container div#list-content table.cbi-section-table thead td.cbi-section-table-cell,[data-page="admin-system-fileassistant"] .cbi-section.fb-container div#list-content table.cbi-section-table tbody td.cbi-section-table-cell{width:232px}[data-page="admin-system-fileassistant"] .cbi-section.fb-container div#list-content tbody tr:nth-child(1) td.parent-icon strong{margin-left:0 !important}[data-page="admin-system-fileassistant"] .cbi-section.fb-container div#list-content tbody td.cbi-value-field.file-icon strong,[data-page="admin-system-fileassistant"] .cbi-section.fb-container div#list-content tbody td.cbi-value-field.folder-icon strong{vertical-align:middle;margin-left:5px}[data-page="admin-system-fileassistant"] .fb-container .panel-container{border-bottom-color:#dee2e6;display:flex}[data-page="admin-system-fileassistant"] .fb-container .panel-container .panel-title{flex:1}[data-page="admin-system-fileassistant"] .fb-container .upload-container{display:flex !important}[data-page="admin-system-fileassistant"] .fb-container .upload-container .upload-file{flex:1;margin-right:0 !important}[data-page="admin-system-opkg"] h2{margin-left:0;color:var(--gray-dark)}[data-page="admin-system-opkg"] input[name="filter_i18n"]{top:2px;vertical-align:text-top}[data-page="admin-system-opkg"] div.btn{line-height:3;display:inline;padding:.3rem .6rem}[data-page="admin-system-opkg"] #maincontent>.container{margin:0 1.25rem 1rem 1.25rem;margin-bottom:1rem}[data-page="admin-system-package-manager"] .controls div:nth-child(5)>label{margin-bottom:5px}[data-page="admin-system-package-manager"] .controls div:nth-child(5)>div label{margin-top:1rem}[data-page="admin-system-package-manager"] .controls div:nth-child(5)>div label input{top:-2px;right:0;vertical-align:middle}[data-page="admin-system-reboot"] p{padding-left:1.5rem}[data-page="admin-system-reboot"] p>span{position:relative;top:.1rem;left:1rem}[data-page="admin-system-reboot"] .cbi-button{background:#fb6340 !important;border-color:#fb6340 !important;margin-left:0 !important}[data-page="admin-system-reboot"] .cbi-button.cbi-button-action.important{align-self:flex-end}#applyreboot-container{margin:2rem}#applyreboot-section{line-height:300%;margin:2rem}[data-page="admin-system-poweroff"] .container h2+br+p{margin-bottom:1rem;padding-left:1.5rem}[data-page="admin-system-poweroffdevice"] .container h2{margin:0}[data-page="admin-system-poweroffdevice"] .container h2+p{margin-bottom:1rem;padding-left:1.5rem}[data-page="admin-system-poweroffdevice"] .container button+div{display:none}[data-page="admin-system-poweroffdevice"] .btn.cbi-button.cbi-button-negative{align-self:flex-end}[data-page="admin-network-network"] .cbi-value-field .cbi-dynlist{padding:0 !important}[data-page="admin-network-network"] .td>.ifacebadge>.cbi-tooltip-container{display:flex}[data-page="admin-network-network"] .td>.ifacebadge>.cbi-tooltip-container img{vertical-align:middle}[data-page="admin-network-network"] div[data-name="_gen_server_keypair"] .cbi-value-title,[data-page="admin-network-network"] div[data-name="_gen_peer_keypair"] .cbi-value-title,[data-page="admin-network-network"] div[data-name="_gen_psk"] .cbi-value-title{height:2.4rem}[data-page="admin-network-network"] #modal_overlay>.modal.cbi-modal>div>p>textarea{height:20em !important;border:1px solid #dee2e6 !important;border-radius:4px}[data-page="admin-network-dhcp"] .cbi-value{padding:0}[data-page="admin-network-dhcp"] [data-tab-active="true"]{padding:1rem 1rem !important}[data-page="admin-network-diagnostics"] .table{box-shadow:none}[data-page="admin-network-diagnostics"] .cbi-section{padding:1rem;font-family:monospace;background:#fff !important}[data-page="admin-network-diagnostics"] textarea{background:transparent;border-radius:.25rem;color:#8898aa;border:1px solid #dee2e6;padding:.5rem}[data-page="admin-network-diagnostics"] .tr{display:flex;flex-direction:row}[data-page="admin-network-diagnostics"] .tr .td{background-color:#fff !important;border-bottom:1px solid #dee2e6 !important;display:flex;flex-direction:row;align-items:center;flex:1}[data-page="admin-network-diagnostics"] .tr .td input{margin:0 !important;border-right:none !important;border-bottom-right-radius:0 !important;border-top-right-radius:0 !important}[data-page="admin-network-diagnostics"] .tr .td .cbi-dropdown,[data-page="admin-network-diagnostics"] .tr .td .cbi-button{height:2.5rem;border-left:none !important;border-bottom-left-radius:0 !important;border-top-left-radius:0 !important}[data-page="admin-network-diagnostics"] .tr .td .cbi-dropdown .more,[data-page="admin-network-diagnostics"] .tr .td .cbi-button .more{display:block;width:1px;height:100%;overflow:hidden;text-indent:5000px;background-color:var(--white)}[data-page="admin-network-diagnostics"] .cbi-dropdown .open{color:var(--white)}[data-page="admin-network-firewall-custom"] #view p,[data-page="admin-status-routes"] #view p{padding:0 1.25rem}[data-page="admin-network-firewall-custom"] #view p textarea,[data-page="admin-status-routes"] #view p textarea{padding:1rem;border-radius:.25rem}[data-page="admin-network-firewall-custom"] #view>h3,[data-page="admin-status-routes"] #view>h3{border-radius:.25rem .25rem 0 0}[data-page="admin-network-firewall-custom"] #view .cbi-tabmenu+div,[data-page="admin-status-routes"] #view .cbi-tabmenu+div{margin-top:-0.5rem}.cbi-section.fade-in .cbi-title{position:relative}.cbi-section.fade-in .cbi-title>div:last-child{position:absolute;right:1.25rem;top:0}.cbi-section.fade-in .cbi-title>div:last-child span{font-size:0;background:none!important;color:#ced4da !important}.cbi-section.fade-in .cbi-title>div:last-child span:after{transition:all .3s;font-family:'argon' !important;content:'\e90f';font-size:1.1rem;padding:.8755rem 0;position:absolute;right:0}.cbi-section.fade-in .cbi-title>div:last-child span[data-style=inactive]:after{transform:rotate(90deg)}[data-page="admin-status"] #view>div:first-child,[data-page="admin-status-overview"] #view>div:first-child{gap:0 !important}[data-page="admin-status-iptables"] .right{margin-bottom:0 !important}[data-page="admin-status-nftables"] .nft-chain-hook{padding:.5rem 1.25rem}[data-page="admin-nlbw-display"] .cbi-section[data-tab="export"]{padding:1.5rem !important}[data-page="admin-nlbw-backup"] form{padding-left:1.5rem}[data-page="admin-services-ddns"] #syslog{overflow:auto;font-size:12px !important}[data-page="admin-services-ttyd"] .container{display:flex;flex-direction:column}[data-page="admin-services-ttyd"] #view{flex:1}[data-page="admin-services-ttyd"] #view iframe{height:100%}#cbi-dockerd .cbi-section{box-sizing:border-box;padding:16px}#cbi-dockerd .cbi-section.cbi-tblsection{padding:0}#cbi-docker .cbi-section .cbi-section-node{box-sizing:border-box;padding:16px}[data-page="admin-services-npc"] div[id="cbid.npc.config.vkey"]{white-space:normal}[data-page="admin-services-npc"] div[id="cbid.npc.config.vkey"] input[id="widget.cbid.npc.config.vkey"][type="password"]{margin-top:0;margin-bottom:0}[data-page="admin-services-npc"] div[id="cbid.npc.config.vkey"] .cbi-button{margin:0;border:none}[data-page="admin-services-npc"] div[id^="cbid.npc."][id$=".server_addr"]:not([id="cbid.npc.config.server_addr"])+br,[data-page="admin-services-npc"] div[id^="cbid.npc."][id$=".vkey"]:not([id="cbid.npc.config.vkey"])+br,[data-page="admin-services-npc"] div[id^="cbid.npc."][id$=".protocol"]:not([id="cbid.npc.config.protocol"])+br,[data-page="admin-services-npc"] div[id^="cbid.npc."][id$=".dns"]:not([id="cbid.npc.config.dns"])+br{display:none}[data-page="admin-services-npc"] div.cbi-value.nowrap{white-space:normal}[data-page^="admin-services-openclash"] .oc{--bg-light:#fff;--bg-gray:#fff;border-radius:8px}[data-page^="admin-services-openclash"] .oc .developer-container{background:transparent}[data-page^="admin-services-openclash"] .oc .announcement-card{width:auto!important;margin:10px!important}[data-page^="admin-services-openclash"] #cbi-openclash>fieldset{background:none;padding:0}[data-page^="admin-services-openclash"] #cbi-openclash>fieldset>table>tbody>tr>td{border:none;padding:0}[data-page^="admin-services-openclash"] #cbi-openclash>fieldset .main-card{border:none;box-shadow:none;padding:0 10px 10px 10px;background:transparent}[data-page^="admin-services-openclash"] #cbi-openclash>fieldset .myip-main-card{margin:0;padding:10px;border:none;background:transparent}[data-page^="admin-services-openclash"] #cbi-openclash>fieldset .myip-main-card .myip-section-title{margin:0;border-width:1px}[data-page^="admin-services-openclash"] .sub-card .card-title{margin-top:5px}[data-page^="admin-services-openclash"] .oc .main-cards-container{margin:0 !important;gap:0 !important}[data-page^="admin-services-openclash"] .oc .config-file-bottom{margin-bottom:5px}[data-page^="admin-services-openclash"] .oc .config-file-bottom .card-actions{margin-bottom:2px !important}[data-page^="admin-services-openclash"] .oc .card-content{align-items:center !important;justify-content:center !important}[data-page^="admin-services-openclash"] .oc .card-controls{align-self:center}[data-page^="admin-services-openclash"] .oc .core-main-controls{justify-content:center !important}[data-page^="admin-services-openclash"] .oc .plugin-toggle-container{margin-left:inherit !important}[data-page^="admin-services-openclash"] .oc .core-status-toggle{flex:0 !important;min-width:auto !important}[data-page^="admin-services-openclash"] .oc .config-upload-content{background:#fff !important}[data-page^="admin-services-openclash"] .oc .subscription-info-container{background:transparent !important}[data-page^="admin-services-openclash"] .cbi-tabmenu>li{border-right:none !important;margin:0 .4rem 0 0 !important}[data-page^="admin-services-openclash"] .cbi-tabmenu>li:last-child{margin-right:0 !important}[data-page^="admin-services-openclash"] #tab-content .dom{padding:0 1rem 1rem 1rem}[data-page^="admin-services-openclash"] .cbi-input-file{padding:.2813rem;width:15rem !important}[data-page^="admin-services-openclash"] [id="container.openclash.config.debug"] fieldset{border:none !important;padding:1rem !important}[data-page^="admin-services-openclash"] #diag-rc-output>pre,[data-page^="admin-services-openclash"] #dns-rc-output>pre{font-size:.875rem;color:#8898aa;border:1px solid #dee2e6;background-color:transparent;border-radius:.25rem;font-family:'Google Sans' !important;box-shadow:none}[data-page^="admin-services-openclash"] #debug-rc-output>textarea{font-family:'Google Sans' !important}[data-page^="admin-services-openclash"] .CodeMirror{font-size:inherit;font-family:'Google Sans' !important}[data-page^="admin-services-openclash"] .cbi-button-up,[data-page^="admin-services-openclash"] .cbi-button-down{padding:.8rem 1.5rem;background-color:#f1f1f1;font-size:0}[data-page^="admin-services-openclash"] select#CORE_VERSION,[data-page^="admin-services-openclash"] select#RELEASE_BRANCH{width:auto}[data-page="admin-vpn-passwall"] h4{background:transparent}.OpenVPN a{line-height:initial !important}#cbi-wireless>#wifi_assoclist_table>.tr{box-shadow:inset 1px -1px 0 #ddd,inset -1px -1px 0 #ddd}#cbi-wireless>#wifi_assoclist_table>.tr.placeholder>.td{right:2rem;bottom:2rem;left:2rem;border-top:thin solid #ddd !important}#cbi-wireless>#wifi_assoclist_table>.tr.table-titles{box-shadow:inset 1px 0 0 #ddd,inset -1px 0 0 #ddd}#cbi-wireless>#wifi_assoclist_table>.tr.table-titles>.th{border-bottom:thin solid #ddd;box-shadow:0 -1px 0 0 #ddd}#wifi_assoclist_table>.tr>.td[data-title="RX Rate / TX Rate"]{width:23rem}#cbi-samba [data-tab="template"] .cbi-value-field{display:block}#cbi-samba [data-tab="template"] .cbi-value-title{width:auto;padding-bottom:.6rem}.node-status-overview>.main fieldset:nth-child(4) .td:nth-child(2),.node-status-processes>.main .table .tr .td:nth-child(3){white-space:normal}div[style*="display:grid;grid-template-columns:repeat"]{display:flex !important;justify-content:space-evenly !important;flex-wrap:wrap}div[style*="display:grid;grid-template-columns:repeat"] .ifacebox{text-align:center;flex-basis:100px}div[style*="display:grid;grid-template-columns:repeat"] .ifacebox .ifacebox-body{font-size:.7rem;padding:.875rem}div[style*="display:grid;grid-template-columns:repeat"] .ifacebox .ifacebox-body .cbi-tooltip-container{font-size:inherit !important}#iptables{margin:0}.Firewall form{margin:2rem 2rem 0 0;padding:0;box-shadow:none}#cbi-firewall-redirect table *,#cbi-network-switch_vlan table *,#cbi-firewall-zone table *{font-size:small}#cbi-firewall-redirect table input[type="text"],#cbi-network-switch_vlan table input[type="text"],#cbi-firewall-zone table input[type="text"]{width:5rem}#cbi-firewall-redirect table select,#cbi-network-switch_vlan table select,#cbi-firewall-zone table select{min-width:3.5rem}#cbi-network-switch_vlan .th,#cbi-network-switch_vlan .td{flex-basis:12%}#cbi-network-switch_vlan .table{display:block}#cbi-network-switch_vlan .td{width:100%}#cbi-firewall-zone .table{display:block}#cbi-firewall-zone .td{width:100%}[data-page^="admin-system-commands"] .panel-title,[data-page^="command-cfg"] .mobile-hide,[data-page^="command-cfg"] .showSide{display:none}#mwan3-service-status{display:flex;justify-content:center;align-items:center;flex-wrap:wrap}#mwan3-service-status>.alert-message{position:static;transform:none}@media all and (-ms-high-contrast:none){.main>.main-left>.nav>.slide>.menu::before{top:30.25%}.main>.main-left>.nav>li:last-child::before{top:20%}.showSide::before{top:-12px}}@media screen and (max-width:1600px){header>.fill>.container>#logo{margin:0 2.5rem 0 .5rem}.main-left{width:calc(0% + 13rem)}.btn:not(button),.label{padding:.5rem .75rem}.cbi-value-title{width:15rem;padding-right:.6rem}.cbi-value-field .cbi-dropdown,.cbi-value-field .cbi-input-select,.cbi-value input[type="text"],.cbi-value input[type="password"],.cbi-value textarea{min-width:18rem}#cbi-firewall-zone .cbi-input-select{min-width:9rem}.cbi-input-textarea{font-size:small}.node-admin-status>.main fieldset li>a{padding:.3rem .6rem}}@media screen and (max-width:1366px){header>.fill>.container{cursor:default}.main-left{width:calc(0% + 13rem)}.tabs>li>a,.cbi-tabmenu>li>a{padding:.2rem .8rem}.panel-title{font-size:1.1rem;padding-bottom:1rem}table{width:100% !important}.table .cbi-input-text{width:100%}.cbi-value-field .cbi-dropdown,.cbi-value-field .cbi-input-select,.cbi-value input[type="text"],.cbi-value input[type="password"]{min-width:16rem}#cbi-firewall-zone .cbi-input-select{min-width:5.5rem}.main>.main-left>.nav>li,.main>.main-left>.nav>li>a,.main .main-left .nav>li>a:first-child,.main>.main-left>.nav>.slide>.menu,.main>.main-left>.nav>li>[data-title="Log_out"]{font-size:.9rem}.main>.main-left>.nav>.slide>.slide-menu>li>a{font-size:.875rem}#modal_overlay{top:0rem}[data-page="admin-network-firewall-forwards"] .table:not(.cbi-section-table){display:block}[data-page="admin-network-firewall-forwards"] .table:not(.cbi-section-table),[data-page="admin-network-firewall-rules"] .table:not(.cbi-section-table),[data-page="admin-network-hosts"] .table,[data-page="admin-network-routes"] .table{overflow-y:visible}.btn:not(button),.cbi-button{font-size:.875rem}}@media screen and (max-width:1152px){header>.fill>.container>#logo{display:none}header>.fill>.container>.brand{position:relative}html,.main{overflow-y:visible}.main>.loading>span{top:25%}.main-left{width:calc(0% + 13rem)}body:not(.logged-in) .showSide{visibility:hidden;width:0;margin:0}.node-main-login>.main .cbi-value-title{text-align:left}.cbi-value-title{width:12rem;padding-right:1rem}.cbi-value-field .cbi-dropdown,.cbi-value-field .cbi-input-select,.cbi-value input[type="text"]{width:16rem;min-width:16rem}.cbi-value input[name^="pw"],.cbi-value input[data-update="change"]:nth-child(2){width:13rem !important;min-width:13rem}#diag-rc-output>pre,#command-rc-output>pre,[data-page="admin-services-wol"] .notice code{font-size:1rem}.table{display:block}.Interfaces .table{overflow-x:hidden}#packages.table{display:grid}.tr{display:flex;flex-direction:row;flex-wrap:wrap}.Overview .table[width="100%"]>.tr{flex-wrap:nowrap}.tr.placeholder{border-bottom:thin solid #ddd}.tr.placeholder>.td,#cbi-firewall .tr>.td,#cbi-network .tr:nth-child(2)>.td,.cbi-section #wifi_assoclist_table .tr>.td{border-top:0}.th,.td{display:inline-block;align-self:flex-start;flex:2 2 10%;text-overflow:ellipsis;word-wrap:break-word}.td select,.td input[type="text"]{width:100%;word-wrap:normal}.td [data-dynlist]>input,.td input.cbi-input-password{width:calc(100% - 1.5rem)}.td[data-type="button"],.td[data-type="fvalue"]{flex:1 1 12.5%;text-align:left}.th.cbi-value-field,.td.cbi-value-field,.th.cbi-section-table-cell,.td.cbi-section-table-cell{flex-basis:auto;padding-top:1rem}.cbi-section-table-row{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;box-shadow:0 2px 2px 0 rgba(0,0,0,0.16),0 0 2px 0 rgba(0,0,0,0.12)}.td.cbi-value-field,.cbi-section-table-cell{display:inline-block;flex:10 10 auto;flex-basis:50%;text-align:center}.td.cbi-section-actions{vertical-align:bottom}.tr.table-titles,.tr.cbi-section-table-titles,.tr.cbi-section-table-descr{display:none}.tr[data-title]::before,.tr.cbi-section-table-titles.named::before{font-size:.9rem;display:block;flex:1 1 100%;border-bottom:thin solid rgba(0,0,0,0.26);background:#e9ecef}.td[data-title],[data-page^="admin-status-realtime"] .td[id]{text-align:left}.td[data-title]::before{display:block}.cbi-button+.cbi-button{margin-left:0}.td.cbi-section-actions>*>*,.td.cbi-section-actions>*>form>*{margin:2.1px 3px}.Firewall form{position:static !important;margin:0 0 2rem 0;padding:2rem;box-shadow:0 2px 2px 0 rgba(0,0,0,0.16),0 0 2px 0 rgba(0,0,0,0.12)}.Firewall form input{width:100% !important;margin:0;margin-top:1rem}.Firewall .center,.Firewall .center::before{text-align:left !important}.btn:not(button),.cbi-button{font-size:.875rem}}@media screen and (max-width:768px){body{font-size:.875rem}.cbi-progressbar::after{font-size:.6rem}.main-left{position:fixed;z-index:100;width:0}.main-left.active{width:13rem}.main-right{width:100%}.main-right.active{overflow-y:hidden}.darkMask.active{display:block}.showSide{padding:.1rem;position:relative;z-index:99;display:inline-block !important}.showSide::before{font-family:'argon' !important;font-style:normal !important;font-weight:normal !important;font-variant:normal !important;text-transform:none !important;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\e20e";font-size:1.7rem}header>.fill>.container>.flex1>.brand{display:inline-block}.main>.main-left>.nav>.slide>.slide-menu>li>a{font-size:.875rem}}@media screen and (max-width:600px){.mobile-hide{display:none}#maincontent>.container{margin:0 1rem 1rem 1rem}.cbi-value-title{text-align:left}.cbi-dynlist p{padding:.5rem 1rem}body{overflow-x:hidden}.node-main-login .main .main-right #maincontent .container .cbi-map .cbi-section .cbi-section-node .cbi-value .cbi-value-field{width:16rem}.node-main-login footer{display:none}.tabs::-webkit-scrollbar,.cbi-tabmenu::-webkit-scrollbar{width:0px;height:0px}.cbi-value-field,.cbi-value-description{display:block !important;padding-left:0 !important;padding-right:0 !important}[data-page="admin-system-admin-password"] .cbi-value-field{display:table-cell !important}.modal.cbi-modal{max-width:100%;max-height:none}.modal{display:flex;align-items:center;flex-wrap:wrap;width:100%;min-width:270px;max-width:600px;min-height:32px;margin:5em auto;padding:1em;border-radius:3px !important;background:#fff;box-shadow:0 2px 2px 0 rgba(0,0,0,0.16),0 0 2px 0 rgba(0,0,0,0.12)}.cbi-dropdown[open]>ul.dropdown{margin-bottom:1rem}.login-page .login-container footer{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}div[style*="display:grid;grid-template-columns:repeat"] .ifacebox{flex-basis:80px}div[style*="display:grid;grid-template-columns:repeat"] .ifacebox .ifacebox-body{padding:.875rem .5rem;font-size:.6rem}.login-page .login-container{margin-left:0rem !important;width:100%}.login-page .login-container .login-form .form-login .input-group::before{color:#525461}.login-page .login-container .login-form .form-login .input-group input{color:#525461;border-bottom:white 1px solid;border-bottom:var(--white) 1px solid;border-radius:0}}@media screen and (min-width:600px){::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar,::-webkit-scrollbar-corner{background:transparent}::-webkit-scrollbar-thumb{background:var(--primary);border-radius:10px}::-webkit-scrollbar-thumb:hover{background:var(--primary)}::-webkit-scrollbar-thumb:active{background:var(--primary)}}
\ No newline at end of file
diff --git a/openwrt-packages/luci-theme-argon/less/page-fix.less b/openwrt-packages/luci-theme-argon/less/page-fix.less
index 34e980008c..64503782c8 100644
--- a/openwrt-packages/luci-theme-argon/less/page-fix.less
+++ b/openwrt-packages/luci-theme-argon/less/page-fix.less
@@ -704,6 +704,11 @@
.developer-container {
background: transparent;
}
+
+ .announcement-card {
+ width: auto!important;
+ margin: 10px!important;
+ }
}
#cbi-openclash>fieldset {
diff --git a/shadowsocks-rust/Cargo.lock b/shadowsocks-rust/Cargo.lock
index 47fbf778e6..aeec3c9f42 100644
--- a/shadowsocks-rust/Cargo.lock
+++ b/shadowsocks-rust/Cargo.lock
@@ -341,19 +341,17 @@ dependencies = [
[[package]]
name = "bson"
-version = "3.0.0"
+version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74c1c1044eb5689370b9da714b543a4a8d15601f4a68538dcadcf9c41b916aff"
+checksum = "b3f109694c4f45353972af96bf97d8a057f82e2d6e496457f4d135b9867a518c"
dependencies = [
"ahash",
"base64",
"bitvec",
- "getrandom 0.2.16",
"getrandom 0.3.4",
"hex",
"indexmap",
"js-sys",
- "once_cell",
"rand",
"serde",
"serde_bytes",
diff --git a/small/luci-app-ssr-plus/root/usr/bin/ssr-rules b/small/luci-app-ssr-plus/root/usr/bin/ssr-rules
index b3f9079acc..8fb813a447 100755
--- a/small/luci-app-ssr-plus/root/usr/bin/ssr-rules
+++ b/small/luci-app-ssr-plus/root/usr/bin/ssr-rules
@@ -184,39 +184,39 @@ ipset_nft() {
# Create necessary collections
for setname in china gmlan fplan bplan whitelist blacklist netflix; do
if ! $NFT list set inet ss_spec $setname >/dev/null 2>&1; then
- $NFT add set inet ss_spec $setname '{ type ipv4_addr; flags interval; auto-merge; }' 2>/dev/null
+ $NFT add set inet ss_spec $setname '{ type ipv4_addr; flags interval; auto-merge; }' 2>/dev/null
else
- $NFT flush set inet ss_spec $setname 2>/dev/null
+ $NFT flush set inet ss_spec $setname 2>/dev/null
fi
- done
+ done
# 批量导入中国IP列表
if [ -f "${china_ip:=/etc/ssrplus/china_ssr.txt}" ]; then
$NFT add element inet ss_spec china "{ $(tr '\n' ',' < "${china_ip}" | sed 's/,$//') }" 2>/dev/null
fi
- # Add IP addresses to sets
- for ip in $LAN_GM_IP; do
+ # Add IP addresses to sets
+ for ip in $LAN_GM_IP; do
[ -n "$ip" ] && $NFT add element inet ss_spec gmlan "{ $ip }" 2>/dev/null
- done
- for ip in $LAN_FP_IP; do
- [ -n "$ip" ] && $NFT add element inet ss_spec fplan "{ $ip }" 2>/dev/null
- done
- for ip in $LAN_BP_IP; do
+ done
+ for ip in $LAN_FP_IP; do
+ [ -n "$ip" ] && $NFT add element inet ss_spec fplan "{ $ip }" 2>/dev/null
+ done
+ for ip in $LAN_BP_IP; do
[ -n "$ip" ] && $NFT add element inet ss_spec bplan "{ $ip }" 2>/dev/null
- done
- for ip in $WAN_BP_IP; do
+ done
+ for ip in $WAN_BP_IP; do
[ -n "$ip" ] && $NFT add element inet ss_spec whitelist "{ $ip }" 2>/dev/null
- done
- for ip in $WAN_FW_IP; do
+ done
+ for ip in $WAN_FW_IP; do
[ -n "$ip" ] && $NFT add element inet ss_spec blacklist "{ $ip }" 2>/dev/null
- done
+ done
- # Create main chain for WAN access control
- if ! $NFT list chain inet ss_spec ss_spec_wan_ac >/dev/null 2>&1; then
+ # Create main chain for WAN access control
+ if ! $NFT list chain inet ss_spec ss_spec_wan_ac >/dev/null 2>&1; then
$NFT add chain inet ss_spec ss_spec_wan_ac 2>/dev/null
- fi
- $NFT flush chain inet ss_spec ss_spec_wan_ac 2>/dev/null
+ fi
+ $NFT flush chain inet ss_spec ss_spec_wan_ac 2>/dev/null
# Create forward chain with better error handling
if ! $NFT list chain inet ss_spec ss_spec_wan_fw >/dev/null 2>&1; then
@@ -227,14 +227,58 @@ ipset_nft() {
fi
# Clear existing rules
$NFT flush chain inet ss_spec ss_spec_wan_fw 2>/dev/null
-
- # Add basic rules
- $NFT add rule inet ss_spec ss_spec_wan_ac tcp dport 53 ip daddr 127.0.0.0/8 return
- $NFT add rule inet ss_spec ss_spec_wan_ac tcp dport != 53 ip daddr "$server" return
- # Set up mode-specific rules
- case "$RUNMODE" in
- router)
+ EXT_ARGS=""
+ if [ -n "$PROXY_PORTS" ]; then
+ PORTS_ARGS=$(echo "$PROXY_PORTS" | sed 's/-m multiport --dports //')
+ if [ -n "$PORTS_ARGS" ]; then
+ EXT_ARGS="th dport { $PORTS_ARGS }"
+ fi
+ fi
+
+ # Add basic rules
+ # ========== 按照正确顺序添加规则 ==========
+
+ # 1. 基础例外规则(最高优先级)
+ $NFT add rule inet ss_spec ss_spec_wan_ac tcp dport 53 ip daddr 127.0.0.0/8 return
+ [ -n "$server" ] && $NFT add rule inet ss_spec ss_spec_wan_ac tcp dport != 53 ip daddr "$server" return
+
+ # 2. 强制访问控制
+ $NFT add rule inet ss_spec ss_spec_wan_ac ip daddr @blacklist jump ss_spec_wan_fw
+ $NFT add rule inet ss_spec ss_spec_wan_ac ip daddr @whitelist return
+ $NFT add rule inet ss_spec ss_spec_wan_ac ip saddr @fplan jump ss_spec_wan_fw
+ $NFT add rule inet ss_spec ss_spec_wan_ac ip saddr @bplan return
+
+ # 3. 特殊功能规则
+ # Music unlocking support
+ if $NFT list set inet ss_spec music >/dev/null 2>&1; then
+ $NFT add rule inet ss_spec ss_spec_wan_ac ip daddr @music return
+ fi
+
+ # Shunt/Netflix rules
+ if [ "$SHUNT_PORT" != "0" ] && [ -f "$SHUNT_LIST" ]; then
+ for ip in $(cat "$SHUNT_LIST" 2>/dev/null); do
+ [ -n "$ip" ] && $NFT add element inet ss_spec netflix "{ $ip }" 2>/dev/null
+ done
+ case "$SHUNT_PORT" in
+ 1)
+ $NFT add rule inet ss_spec ss_spec_wan_ac meta l4proto tcp $EXT_ARGS ip daddr @netflix counter redirect to :$local_port
+ ;;
+ *)
+ $NFT add rule inet ss_spec ss_spec_wan_ac meta l4proto tcp $EXT_ARGS ip daddr @netflix counter redirect to :$SHUNT_PORT
+ if [ "$SHUNT_PROXY" = "1" ]; then
+ $NFT add rule inet ss_spec ss_spec_wan_ac meta l4proto tcp $EXT_ARGS ip daddr $SHUNT_IP counter redirect to :$local_port
+ else
+ [ -n "$SHUNT_IP" ] && $NFT add element inet ss_spec whitelist "{ $SHUNT_IP }" 2>/dev/null
+ fi
+ ;;
+ esac
+ fi
+
+ # 4. 模式特定规则
+ # Set up mode-specific rules
+ case "$RUNMODE" in
+ router)
if ! $NFT list set inet ss_spec ss_spec_wan_ac >/dev/null 2>&1; then
$NFT add set inet ss_spec ss_spec_wan_ac '{ type ipv4_addr; flags interval; auto-merge; }'
else
@@ -246,71 +290,36 @@ ipset_nft() {
done
$NFT add rule inet ss_spec ss_spec_wan_ac ip daddr @ss_spec_wan_ac return
- $NFT add rule inet ss_spec ss_spec_wan_ac ip daddr @china return 2>/dev/null
+ $NFT add rule inet ss_spec ss_spec_wan_ac ip daddr @china return
if $NFT list chain inet ss_spec ss_spec_wan_fw >/dev/null 2>&1; then
$NFT add rule inet ss_spec ss_spec_wan_ac ip saddr @gmlan ip daddr != @china jump ss_spec_wan_fw
$NFT add rule inet ss_spec ss_spec_wan_ac jump ss_spec_wan_fw
fi
;;
- gfw)
+ gfw)
if ! $NFT list set inet ss_spec gfwlist >/dev/null 2>&1; then
$NFT add set inet ss_spec gfwlist '{ type ipv4_addr; flags interval; auto-merge; }' 2>/dev/null
fi
- $NFT add rule inet ss_spec ss_spec_wan_ac ip daddr @china return 2>/dev/null
- $NFT add rule inet ss_spec ss_spec_wan_ac ip daddr @gfwlist jump ss_spec_wan_fw 2>/dev/null
- if $NFT list chain inet ss_spec ss_spec_wan_fw >/dev/null 2>&1; then
- $NFT add rule inet ss_spec ss_spec_wan_ac ip saddr @gmlan ip daddr != @china jump ss_spec_wan_fw
- fi
+ $NFT add rule inet ss_spec ss_spec_wan_ac ip daddr @china return
+ $NFT add rule inet ss_spec ss_spec_wan_ac ip daddr @gfwlist jump ss_spec_wan_fw
+ $NFT add rule inet ss_spec ss_spec_wan_ac ip saddr @gmlan ip daddr != @china jump ss_spec_wan_fw
;;
- oversea)
+ oversea)
if ! $NFT list set inet ss_spec oversea >/dev/null 2>&1; then
$NFT add set inet ss_spec oversea '{ type ipv4_addr; flags interval; auto-merge; }' 2>/dev/null
fi
- if $NFT list chain inet ss_spec ss_spec_wan_fw >/dev/null 2>&1; then
- $NFT insert rule inet ss_spec ss_spec_wan_ac ip daddr @oversea jump ss_spec_wan_fw 2>/dev/null
- $NFT add rule inet ss_spec ss_spec_wan_ac ip saddr @gmlan jump ss_spec_wan_fw 2>/dev/null
- $NFT add rule inet ss_spec ss_spec_wan_ac ip daddr @china jump ss_spec_wan_fw 2>/dev/null
- fi
+ $NFT add rule inet ss_spec ss_spec_wan_ac ip daddr @oversea jump ss_spec_wan_fw
+ $NFT add rule inet ss_spec ss_spec_wan_ac ip saddr @gmlan jump ss_spec_wan_fw
+ $NFT add rule inet ss_spec ss_spec_wan_ac ip daddr @china jump ss_spec_wan_fw
;;
- all)
+ all)
if $NFT list chain inet ss_spec ss_spec_wan_fw >/dev/null 2>&1; then
$NFT add rule inet ss_spec ss_spec_wan_ac jump ss_spec_wan_fw
fi
;;
- esac
+ esac
- # Access control rules
- $NFT add rule inet ss_spec ss_spec_wan_ac ip saddr @fplan jump ss_spec_wan_fw
- $NFT add rule inet ss_spec ss_spec_wan_ac ip saddr @bplan return
- $NFT add rule inet ss_spec ss_spec_wan_ac ip daddr @blacklist jump ss_spec_wan_fw
- $NFT add rule inet ss_spec ss_spec_wan_ac ip daddr @whitelist return
-
- # Music unlocking support
- if $NFT list set inet ss_spec music >/dev/null 2>&1; then
- $NFT insert rule inet ss_spec ss_spec_wan_ac ip daddr @music return 2>/dev/null
- fi
-
- # Shunt/Netflix rules
- if [ "$SHUNT_PORT" != "0" ] && [ -f "$SHUNT_LIST" ]; then
- for ip in $(cat "$SHUNT_LIST" 2>/dev/null); do
- [ -n "$ip" ] && $NFT add element inet ss_spec netflix "{ $ip }" 2>/dev/null
- done
- case "$SHUNT_PORT" in
- 1)
- $NFT insert rule inet ss_spec ss_spec_wan_ac ip daddr @netflix meta l4proto tcp redirect to :"$local_port"
- ;;
- *)
- $NFT insert rule inet ss_spec ss_spec_wan_ac ip daddr @netflix meta l4proto tcp redirect to :"$SHUNT_PORT"
- if [ "$SHUNT_PROXY" = "1" ]; then
- $NFT insert rule inet ss_spec ss_spec_wan_ac ip daddr "$SHUNT_IP" meta l4proto tcp redirect to :"$local_port"
- else
- [ -n "$SHUNT_IP" ] && $NFT add element inet ss_spec whitelist "{ $SHUNT_IP }" 2>/dev/null
- fi
- ;;
- esac
- fi
-
- return $?
+ return $?
}
ipset_iptables() {
@@ -403,10 +412,10 @@ fw_rule_nft() {
# redirect/translation: when PROXY_PORTS present, redirect those tcp ports to local_port
if [ -n "$PROXY_PORTS" ]; then
PORTS=$(echo "$PROXY_PORTS" | sed 's/-m multiport --dports //')
- RULE="tcp dport { $PORTS } redirect to :"$local_port""
+ RULE="tcp dport { $PORTS } counter redirect to :"$local_port""
else
# default: redirect everything except ssh(22)
- RULE="tcp dport != 22 redirect to :"$local_port""
+ RULE="tcp dport != 22 counter redirect to :"$local_port""
fi
if ! $NFT list chain inet ss_spec ss_spec_wan_fw 2>/dev/null | grep -q "$RULE"; then
if ! $NFT add rule inet ss_spec ss_spec_wan_fw $RULE 2>/dev/null; then
@@ -475,28 +484,32 @@ ac_rule_nft() {
# 创建ss_spec_prerouting链
if ! $NFT list chain inet ss_spec ss_spec_prerouting >/dev/null 2>&1; then
- $NFT add chain inet ss_spec ss_spec_prerouting '{ type nat hook prerouting priority -150; policy accept; }'
+ $NFT add chain inet ss_spec ss_spec_prerouting '{ type nat hook prerouting priority 0; policy accept; }'
fi
$NFT flush chain inet ss_spec ss_spec_prerouting 2>/dev/null
# 创建ss_spec_output链
if ! $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
- $NFT add chain inet ss_spec ss_spec_output '{ type nat hook output priority -100; policy accept; }'
+ $NFT add chain inet ss_spec ss_spec_output '{ type nat hook output priority 0; policy accept; }'
fi
$NFT flush chain inet ss_spec ss_spec_output 2>/dev/null
# Build a rule in the prerouting hook chain that jumps to business chain with conditions
+ EXT_ARGS=""
if [ -n "$PROXY_PORTS" ]; then
- EXT_ARGS=$(echo "$PROXY_PORTS" | sed 's/-m multiport --dports //')
+ PORTS_ARGS=$(echo "$PROXY_PORTS" | sed 's/-m multiport --dports //')
+ if [ -n "$PORTS_ARGS" ]; then
+ EXT_ARGS="th dport { $PORTS_ARGS }"
+ fi
fi
if [ -z "$Interface" ]; then
# generic prerouting jump already exists (see ipset_nft), but if we have MATCH_SET_CONDITION we add a more specific rule
if [ -n "$MATCH_SET" ]; then
# add a more specific rule at the top of ss_spec_prerouting
- $NFT insert rule inet ss_spec ss_spec_prerouting meta l4proto tcp th dport { $EXT_ARGS } $MATCH_SET jump ss_spec_wan_ac comment "\"$TAG\"" 2>/dev/null
+ $NFT insert rule inet ss_spec ss_spec_prerouting meta l4proto tcp $EXT_ARGS $MATCH_SET jump ss_spec_wan_ac comment "\"$TAG\"" 2>/dev/null
else
- $NFT insert rule inet ss_spec ss_spec_prerouting meta l4proto tcp th dport { $EXT_ARGS } jump ss_spec_wan_ac comment "\"$TAG\"" 2>/dev/null
+ $NFT insert rule inet ss_spec ss_spec_prerouting meta l4proto tcp $EXT_ARGS jump ss_spec_wan_ac comment "\"$TAG\"" 2>/dev/null
fi
else
# For each Interface, find its actual ifname and add an iifname-limited prerouting rule
@@ -505,9 +518,9 @@ ac_rule_nft() {
[ -z "$IFNAME" ] && IFNAME=$(uci -P /var/state get network."$name".device 2>/dev/null)
if [ -n "$IFNAME" ]; then
if [ -n "$MATCH_SET" ]; then
- $NFT insert rule inet ss_spec ss_spec_prerouting meta iifname "$IFNAME" meta l4proto tcp th dport { $EXT_ARGS } $MATCH_SET jump ss_spec_wan_ac comment "\"$TAG\"" 2>/dev/null
+ $NFT insert rule inet ss_spec ss_spec_prerouting meta iifname "$IFNAME" meta l4proto tcp $EXT_ARGS $MATCH_SET jump ss_spec_wan_ac comment "\"$TAG\"" 2>/dev/null
else
- $NFT insert rule inet ss_spec ss_spec_prerouting meta iifname "$IFNAME" meta l4proto tcp th dport { $EXT_ARGS } jump ss_spec_wan_ac comment "\"$TAG\"" 2>/dev/null
+ $NFT insert rule inet ss_spec ss_spec_prerouting meta iifname "$IFNAME" meta l4proto tcp $EXT_ARGS jump ss_spec_wan_ac comment "\"$TAG\"" 2>/dev/null
fi
fi
done
@@ -516,7 +529,7 @@ ac_rule_nft() {
case "$OUTPUT" in
1)
# create output hook chain & route output traffic into router chain
- $NFT insert rule inet ss_spec ss_spec_output meta l4proto tcp th dport { $EXT_ARGS } jump ss_spec_wan_ac comment "\"$TAG\"" 2>/dev/null
+ $NFT insert rule inet ss_spec ss_spec_output meta l4proto tcp $EXT_ARGS jump ss_spec_wan_ac comment "\"$TAG\"" 2>/dev/null
;;
2)
# router mode output chain: create ssr_gen_router set & router chain
@@ -527,7 +540,7 @@ ac_rule_nft() {
$NFT add chain inet ss_spec ss_spec_router 2>/dev/null
$NFT add rule inet ss_spec ss_spec_router ip daddr @ssr_gen_router return 2>/dev/null
$NFT add rule inet ss_spec ss_spec_router jump ss_spec_wan_fw 2>/dev/null
- $NFT add rule inet ss_spec ss_spec_output meta l4proto tcp th dport { $EXT_ARGS } jump ss_spec_router comment "\"$TAG\"" 2>/dev/null
+ $NFT add rule inet ss_spec ss_spec_output meta l4proto tcp $EXT_ARGS jump ss_spec_router comment "\"$TAG\"" 2>/dev/null
;;
esac
return 0
@@ -601,12 +614,21 @@ tp_rule_nft() {
fi
local MATCH_SET=""
- local EXT_ARGS=""
+ EXT_ARGS=""
if [ -n "$PROXY_PORTS" ]; then
- EXT_ARGS=$(echo "$PROXY_PORTS" | sed 's/-m multiport --dports //')
+ PORTS_ARGS=$(echo "$PROXY_PORTS" | sed 's/-m multiport --dports //')
+ if [ -n "$PORTS_ARGS" ]; then
+ EXT_ARGS="th dport { $PORTS_ARGS }"
+ else
+ EXT_ARGS=""
+ fi
fi
+ # 有端口 => 1,无端口 => 0
+ HAS_PORTS=0
+ [ -n "$EXT_ARGS" ] && HAS_PORTS=1
+
if [ -n "$LAN_AC_IP" ]; then
# Create LAN access control set if needed
if ! $NFT list set ip ss_spec_mangle ss_spec_lan_ac >/dev/null 2>&1; then
@@ -654,15 +676,15 @@ tp_rule_nft() {
$NFT flush chain ip ss_spec_mangle ss_spec_tproxy 2>/dev/null
fi
- # basic return rules in tproxy chain
- $NFT add rule ip ss_spec_mangle ss_spec_tproxy udp dport 53 return 2>/dev/null
-
if $NFT list chain ip ss_spec_mangle ss_spec_tproxy >/dev/null 2>&1; then
for net in 0.0.0.0/8 10.0.0.0/8 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/4 240.0.0.0/4; do
$NFT add rule ip ss_spec_mangle ss_spec_tproxy ip daddr $net return 2>/dev/null
done
fi
+ # basic return rules in tproxy chain
+ $NFT add rule ip ss_spec_mangle ss_spec_tproxy udp dport 53 return 2>/dev/null
+
# avoid redirecting to udp server address
if [ -n "$server" ]; then
$NFT add rule ip ss_spec_mangle ss_spec_tproxy udp dport != 53 ip daddr "$server" return 2>/dev/null
@@ -679,10 +701,10 @@ tp_rule_nft() {
# access control and tproxy rules
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip saddr @bplan return 2>/dev/null
- if [ -n "$EXT_ARGS" ]; then
- $NFT add rule ip ss_spec_mangle ss_spec_tproxy udp dport { $EXT_ARGS } ip saddr @fplan tproxy to :"$LOCAL_PORT" meta mark set 0x01
+ if [ $HAS_PORTS -eq 1 ]; then
+ $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp $EXT_ARGS ip saddr @fplan counter tproxy ip to :"$LOCAL_PORT" meta mark set 0x01
else
- $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip saddr @fplan tproxy to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
+ $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip saddr @fplan counter tproxy ip to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
fi
# Handle different run modes for nftables
@@ -700,21 +722,21 @@ tp_rule_nft() {
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip daddr @ss_spec_wan_ac return 2>/dev/null
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip daddr @china return 2>/dev/null
- $NFT add rule ip ss_spec_mangle ss_spec_tproxy udp dport 80 drop 2>/dev/null
- $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip saddr @gmlan ip daddr != @china tproxy to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
- if [ -n "$EXT_ARGS" ]; then
- $NFT add rule ip ss_spec_mangle ss_spec_tproxy udp dport { $EXT_ARGS } ip daddr != @ss_spec_wan_ac tproxy to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
+ $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp dport 80 drop 2>/dev/null
+ $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip saddr @gmlan ip daddr != @china counter tproxy ip to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
+ if [ $HAS_PORTS -eq 1 ]; then
+ $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp $EXT_ARGS ip daddr != @ss_spec_wan_ac counter tproxy ip to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
else
- $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip daddr != @ss_spec_wan_ac tproxy to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
+ $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip daddr != @ss_spec_wan_ac counter tproxy ip to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
fi
;;
gfw)
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip daddr @china return 2>/dev/null
- $NFT add rule ip ss_spec_mangle ss_spec_tproxy udp dport 80 drop 2>/dev/null
- if [ -n "$EXT_ARGS" ]; then
- $NFT add rule ip ss_spec_mangle ss_spec_tproxy udp dport { $EXT_ARGS } ip daddr @gfwlist tproxy to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
+ $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp dport 80 drop 2>/dev/null
+ if [ $HAS_PORTS -eq 1 ]; then
+ $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp $EXT_ARGS ip daddr @gfwlist counter tproxy ip to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
fi
- $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip saddr @gmlan ip daddr != @china tproxy to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
+ $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip saddr @gmlan ip daddr != @china counter tproxy ip to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
;;
oversea)
if ! $NFT list set ip ss_spec_mangle oversea >/dev/null 2>&1; then
@@ -723,17 +745,17 @@ tp_rule_nft() {
if ! $NFT list set ip ss_spec_mangle china >/dev/null 2>&1; then
$NFT add set ip ss_spec_mangle china '{ type ipv4_addr; flags interval; auto-merge; }' 2>/dev/null
fi
- if [ -n "$EXT_ARGS" ]; then
- $NFT add rule ip ss_spec_mangle ss_spec_tproxy udp dport { $EXT_ARGS } ip saddr @oversea tproxy to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
- $NFT add rule ip ss_spec_mangle ss_spec_tproxy udp dport { $EXT_ARGS } ip daddr @china tproxy to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
+ if [ $HAS_PORTS -eq 1 ]; then
+ $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp $EXT_ARGS ip saddr @oversea counter tproxy ip to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
+ $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp $EXT_ARGS ip daddr @china counter tproxy ip to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
fi
- $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip saddr @gmlan tproxy to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
+ $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip saddr @gmlan counter tproxy ip to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
;;
all)
- if [ -n "$EXT_ARGS" ]; then
- $NFT add rule ip ss_spec_mangle ss_spec_tproxy udp dport { $EXT_ARGS } tproxy to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
+ if [ $HAS_PORTS -eq 1 ]; then
+ $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp $EXT_ARGS counter tproxy ip to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
else
- $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp tproxy to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
+ $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp counter tproxy ip to :"$LOCAL_PORT" meta mark set 0x01 2>/dev/null
fi
;;
esac
@@ -747,9 +769,9 @@ tp_rule_nft() {
if [ -z "$Interface" ]; then
# 全局规则
if [ -n "$MATCH_SET" ]; then
- $NFT add rule ip ss_spec_mangle prerouting udp dport { $EXT_ARGS } $MATCH_SET jump ss_spec_tproxy comment "\"$TAG\"" 2>/dev/null
+ $NFT add rule ip ss_spec_mangle prerouting meta l4proto udp $EXT_ARGS $MATCH_SET jump ss_spec_tproxy comment "\"$TAG\"" 2>/dev/null
else
- $NFT add rule ip ss_spec_mangle prerouting udp dport { $EXT_ARGS } jump ss_spec_tproxy comment "\"$TAG\"" 2>/dev/null
+ $NFT add rule ip ss_spec_mangle prerouting meta l4proto udp $EXT_ARGS jump ss_spec_tproxy comment "\"$TAG\"" 2>/dev/null
fi
else
# 指定接口
@@ -758,9 +780,9 @@ tp_rule_nft() {
[ -z "$IFNAME" ] && IFNAME=$(uci -P /var/state get network."$name".device 2>/dev/null)
if [ -n "$IFNAME" ]; then
if [ -n "$MATCH_SET" ]; then
- $NFT add rule ip ss_spec_mangle prerouting meta iifname "$IFNAME" udp dport { $EXT_ARGS } $MATCH_SET jump ss_spec_tproxy comment "\"$TAG\"" 2>/dev/null
+ $NFT add rule ip ss_spec_mangle prerouting meta iifname "$IFNAME" meta l4proto udp $EXT_ARGS $MATCH_SET jump ss_spec_tproxy comment "\"$TAG\"" 2>/dev/null
else
- $NFT add rule ip ss_spec_mangle prerouting meta iifname "$IFNAME" udp dport { $EXT_ARGS } jump ss_spec_tproxy comment "\"$TAG\"" 2>/dev/null
+ $NFT add rule ip ss_spec_mangle prerouting meta iifname "$IFNAME" meta l4proto udp $EXT_ARGS jump ss_spec_tproxy comment "\"$TAG\"" 2>/dev/null
fi
fi
done
diff --git a/small/v2ray-geodata/Makefile b/small/v2ray-geodata/Makefile
index 06a296f628..83af933b24 100644
--- a/small/v2ray-geodata/Makefile
+++ b/small/v2ray-geodata/Makefile
@@ -12,7 +12,7 @@ PKG_MAINTAINER:=Tianling Shen
include $(INCLUDE_DIR)/package.mk
-GEOIP_VER:=202511050144
+GEOIP_VER:=202511210201
GEOIP_FILE:=geoip.dat.$(GEOIP_VER)
define Download/geoip
URL:=https://github.com/v2fly/geoip/releases/download/$(GEOIP_VER)/
@@ -21,7 +21,7 @@ define Download/geoip
HASH:=2445b44d9ae3ab9a867c9d1e0e244646c4c378622e14b9afaf3658ecf46a40b9
endef
-GEOSITE_VER:=20251120132211
+GEOSITE_VER:=20251121020256
GEOSITE_FILE:=dlc.dat.$(GEOSITE_VER)
define Download/geosite
URL:=https://github.com/v2fly/domain-list-community/releases/download/$(GEOSITE_VER)/
diff --git a/v2ray-core/go.mod b/v2ray-core/go.mod
index 91d4fef448..c503eaf49d 100644
--- a/v2ray-core/go.mod
+++ b/v2ray-core/go.mod
@@ -37,10 +37,10 @@ require (
github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432
go.starlark.net v0.0.0-20230612165344-9532f5667272
go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35
- golang.org/x/crypto v0.43.0
- golang.org/x/net v0.46.0
- golang.org/x/sync v0.17.0
- golang.org/x/sys v0.37.0
+ golang.org/x/crypto v0.45.0
+ golang.org/x/net v0.47.0
+ golang.org/x/sync v0.18.0
+ golang.org/x/sys v0.38.0
google.golang.org/grpc v1.76.0
google.golang.org/protobuf v1.36.10
gopkg.in/yaml.v3 v3.0.1
@@ -87,10 +87,10 @@ require (
github.com/xtaci/smux v1.5.24 // indirect
go.uber.org/mock v0.5.2 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
- golang.org/x/mod v0.28.0 // indirect
- golang.org/x/text v0.30.0 // indirect
+ golang.org/x/mod v0.29.0 // indirect
+ golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.5.0 // indirect
- golang.org/x/tools v0.37.0 // indirect
+ golang.org/x/tools v0.38.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
nhooyr.io/websocket v1.8.6 // indirect
)
diff --git a/v2ray-core/go.sum b/v2ray-core/go.sum
index 24982ece7d..1244011d87 100644
--- a/v2ray-core/go.sum
+++ b/v2ray-core/go.sum
@@ -590,8 +590,8 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
-golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
-golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -620,8 +620,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
-golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
+golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
+golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -653,8 +653,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
-golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
-golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
+golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
+golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -668,8 +668,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
-golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
+golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -717,8 +717,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
-golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
+golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -738,8 +738,8 @@ golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
-golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
+golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
+golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -775,8 +775,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
-golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
+golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
+golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/v2ray-core/main/commands/all/engineering/engineering.go b/v2ray-core/main/commands/all/engineering/engineering.go
index d7b62d707b..c7f8e7600a 100644
--- a/v2ray-core/main/commands/all/engineering/engineering.go
+++ b/v2ray-core/main/commands/all/engineering/engineering.go
@@ -19,3 +19,7 @@ var cmdEngineering = &base.Command{
func init() {
base.RegisterCommand(cmdEngineering)
}
+
+func AddCommand(cmd *base.Command) {
+ cmdEngineering.Commands = append(cmdEngineering.Commands, cmd)
+}
diff --git a/v2ray-core/main/commands/all/engineering/generateRandomData/errors.generated.go b/v2ray-core/main/commands/all/engineering/generateRandomData/errors.generated.go
new file mode 100644
index 0000000000..e83bae8395
--- /dev/null
+++ b/v2ray-core/main/commands/all/engineering/generateRandomData/errors.generated.go
@@ -0,0 +1,9 @@
+package generateRandomData
+
+import "github.com/v2fly/v2ray-core/v5/common/errors"
+
+type errPathObjHolder struct{}
+
+func newError(values ...interface{}) *errors.Error {
+ return errors.New(values...).WithPathObj(errPathObjHolder{})
+}
diff --git a/v2ray-core/main/commands/all/engineering/generateRandomData/generateRandomData.go b/v2ray-core/main/commands/all/engineering/generateRandomData/generateRandomData.go
new file mode 100644
index 0000000000..07232314fb
--- /dev/null
+++ b/v2ray-core/main/commands/all/engineering/generateRandomData/generateRandomData.go
@@ -0,0 +1,65 @@
+package generateRandomData
+
+import (
+ "crypto/rand"
+ "encoding/base64"
+ "flag"
+ "fmt"
+
+ "github.com/v2fly/v2ray-core/v5/main/commands/all/engineering"
+ "github.com/v2fly/v2ray-core/v5/main/commands/base"
+)
+
+//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen
+
+var length *int
+
+var cmdGenerateRandomData = &base.Command{
+ UsageLine: "{{.Exec}} engineering generate-random-data",
+ Short: "generate random data and output as base64",
+ Long: `
+Generate random data of specified length and output as base64 encoded string.
+
+Usage:
+ {{.Exec}} engineering generate-random-data -length
+
+Options:
+ -length
+ The number of random bytes to generate (required)
+
+Example:
+ {{.Exec}} engineering generate-random-data -length 32
+`,
+ Flag: func() flag.FlagSet {
+ fs := flag.NewFlagSet("", flag.ExitOnError)
+ length = fs.Int("length", 0, "number of random bytes to generate")
+ return *fs
+ }(),
+ Run: func(cmd *base.Command, args []string) {
+ err := cmd.Flag.Parse(args)
+ if err != nil {
+ base.Fatalf("failed to parse flags: %v", err)
+ }
+
+ if *length <= 0 {
+ base.Fatalf("length must be a positive integer, got: %d", *length)
+ }
+
+ // Generate random data
+ randomData := make([]byte, *length)
+ _, err = rand.Read(randomData)
+ if err != nil {
+ base.Fatalf("failed to generate random data: %v", err)
+ }
+
+ // Encode to base64
+ encoded := base64.StdEncoding.EncodeToString(randomData)
+
+ // Output the result
+ fmt.Println(encoded)
+ },
+}
+
+func init() {
+ engineering.AddCommand(cmdGenerateRandomData)
+}
diff --git a/v2ray-core/main/distro/all/all.go b/v2ray-core/main/distro/all/all.go
index eee2a5eb31..303e559b81 100644
--- a/v2ray-core/main/distro/all/all.go
+++ b/v2ray-core/main/distro/all/all.go
@@ -81,6 +81,8 @@ import (
_ "github.com/v2fly/v2ray-core/v5/transport/internet/request/stereotype/meek"
_ "github.com/v2fly/v2ray-core/v5/transport/internet/request/stereotype/mekya"
+ _ "github.com/v2fly/v2ray-core/v5/transport/internet/request/roundtripperreverserserver/clicommand"
+
_ "github.com/v2fly/v2ray-core/v5/transport/internet/dtls"
_ "github.com/v2fly/v2ray-core/v5/transport/internet/httpupgrade"
@@ -111,6 +113,7 @@ import (
// engineering commands
_ "github.com/v2fly/v2ray-core/v5/main/commands/all/engineering"
+ _ "github.com/v2fly/v2ray-core/v5/main/commands/all/engineering/generateRandomData"
// Commands that rely on jsonv4 format This disable selective compile
_ "github.com/v2fly/v2ray-core/v5/main/commands/all/api/jsonv4"
diff --git a/v2ray-core/transport/internet/request/reverser.go b/v2ray-core/transport/internet/request/reverser.go
new file mode 100644
index 0000000000..67a4a3a21f
--- /dev/null
+++ b/v2ray-core/transport/internet/request/reverser.go
@@ -0,0 +1,12 @@
+package request
+
+import "context"
+
+type ReverserImpl interface {
+ OnOtherRoundTrip(ctx context.Context, req Request, opts ...RoundTripperOption) (resp Response, err error)
+ OnAuthenticatedServerIntentRoundTrip(ctx context.Context, serverPublic []byte, req Request, opts ...RoundTripperOption) (resp Response, err error)
+}
+
+type ReverserAccessChecker interface {
+ CheckReverserAccess(ctx context.Context, serverKey []byte) (clientKey []byte, err error)
+}
diff --git a/v2ray-core/transport/internet/request/roundtripper/httprt/httprt.go b/v2ray-core/transport/internet/request/roundtripper/httprt/httprt.go
index e98d80daf0..3dacadcde9 100644
--- a/v2ray-core/transport/internet/request/roundtripper/httprt/httprt.go
+++ b/v2ray-core/transport/internet/request/roundtripper/httprt/httprt.go
@@ -82,7 +82,7 @@ func (h *httpTripperClient) RoundTrip(ctx context.Context, req request.Request,
if err != nil {
return request.Response{}, err
}
- return request.Response{Data: result}, err
+ return request.Response{Data: result}, nil
}
_, err = io.Copy(streamingWriter, httpResp.Body)
if err != nil {
@@ -170,10 +170,10 @@ func (h *httpTripperServer) Start() error {
h.listener = listener
go func() {
httpServer := http.Server{
- ReadHeaderTimeout: 15 * time.Second,
- ReadTimeout: 15 * time.Second,
- WriteTimeout: 10 * time.Second,
- IdleTimeout: 30 * time.Second,
+ ReadHeaderTimeout: 240 * time.Second,
+ ReadTimeout: 240 * time.Second,
+ WriteTimeout: 240 * time.Second,
+ IdleTimeout: 240 * time.Second,
}
httpServer.Handler = h
err := httpServer.Serve(h.listener)
diff --git a/v2ray-core/transport/internet/request/roundtripperreverserserver/accesschecker_password.go b/v2ray-core/transport/internet/request/roundtripperreverserserver/accesschecker_password.go
new file mode 100644
index 0000000000..364b4aa514
--- /dev/null
+++ b/v2ray-core/transport/internet/request/roundtripperreverserserver/accesschecker_password.go
@@ -0,0 +1,127 @@
+package roundtripperreverserserver
+
+import (
+ "bytes"
+ "context"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/hkdf"
+ "crypto/sha256"
+ "math"
+
+ "github.com/v2fly/struc"
+)
+
+type PasswordAccessChecker struct {
+ Password string
+
+ block cipher.Block
+}
+
+// NewPasswordAccessChecker creates a PasswordAccessChecker from the given password
+// and initializes its internal cipher block. It returns an error if initialization
+// fails.
+func NewPasswordAccessChecker(password string) (*PasswordAccessChecker, error) {
+ p := &PasswordAccessChecker{Password: password}
+ if err := p.init(); err != nil {
+ return nil, err
+ }
+ return p, nil
+}
+
+type tokenUnpacked struct {
+ UserID int64 `struc:"int64,little"`
+ Check uint64 `struc:"uint64,little"`
+}
+
+func (p *PasswordAccessChecker) CheckReverserAccess(ctx context.Context, serverKey []byte) (clientKey []byte, err error) {
+ if len(serverKey) != 16 {
+ return nil, newError("invalid server key length")
+ }
+ buffer := make([]byte, 16)
+ // Decrypt into buffer from serverKey
+ p.block.Decrypt(buffer, serverKey)
+
+ token := &tokenUnpacked{}
+
+ err = struc.Unpack(bytes.NewReader(buffer), token)
+ if err != nil {
+ return nil, newError("failed to unpack token").Base(err)
+ }
+
+ expectedCheck := uint64(0)
+ if token.Check != expectedCheck {
+ return nil, newError("invalid token check value")
+ }
+
+ if token.UserID == int64(math.MinInt64) {
+ return nil, newError("invalid token userID: MinInt64")
+ }
+
+ if token.UserID < 0 {
+ return nil, newError("invalid token userID for server: client token has negative userID")
+ }
+
+ // pack client token into a buffer with capacity 16
+ buf := bytes.NewBuffer(make([]byte, 0, 16))
+ err = struc.Pack(buf, &tokenUnpacked{
+ UserID: -token.UserID,
+ Check: expectedCheck,
+ })
+ if err != nil {
+ return nil, newError("failed to pack client token").Base(err)
+ }
+ clientKeyBuffer := buf.Bytes()
+ if len(clientKeyBuffer) != 16 {
+ return nil, newError("invalid packed client token length")
+ }
+ // encrypt into a fresh slice to avoid overlapping issues
+ encrypted := make([]byte, 16)
+ p.block.Encrypt(encrypted, clientKeyBuffer)
+ return encrypted, nil
+}
+
+func (p *PasswordAccessChecker) GenerateToken(userID int64) ([]byte, error) {
+ if userID == int64(math.MinInt64) {
+ return nil, newError("userID cannot be MinInt64")
+ }
+ expectedCheck := uint64(0)
+ // pack token into a buffer with capacity 16
+ buf := bytes.NewBuffer(make([]byte, 0, 16))
+ err := struc.Pack(buf, &tokenUnpacked{
+ UserID: userID,
+ Check: expectedCheck,
+ })
+ if err != nil {
+ return nil, newError("failed to pack token").Base(err)
+ }
+ buffer := buf.Bytes()
+ if len(buffer) != 16 {
+ return nil, newError("invalid packed token length")
+ }
+ encrypted := make([]byte, 16)
+ p.block.Encrypt(encrypted, buffer)
+ return encrypted, nil
+}
+
+func (p *PasswordAccessChecker) init() error {
+ block, err := createBlockFromPassword(p.Password)
+ if err != nil {
+ return newError("failed to create block from password").Base(err)
+ }
+ p.block = block
+ return nil
+}
+
+func createBlockFromPassword(password string) (cipher.Block, error) {
+ // derive a 16-byte key from the password with hkdf
+ key, err := hkdf.Expand(sha256.New, []byte(password), "v2ray-hd87BYQL-aBzumdEh-Yv4E6Rdu:request-roundtripperreverserserver"+"createBlockFromPassword", 16)
+ if err != nil {
+ return nil, newError("unable to derive key from password").Base(err)
+ }
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, newError("unable to create AES cipher").Base(err)
+ }
+ return block, nil
+}
diff --git a/v2ray-core/transport/internet/request/roundtripperreverserserver/accesschecker_password_test.go b/v2ray-core/transport/internet/request/roundtripperreverserserver/accesschecker_password_test.go
new file mode 100644
index 0000000000..2e14b7c991
--- /dev/null
+++ b/v2ray-core/transport/internet/request/roundtripperreverserserver/accesschecker_password_test.go
@@ -0,0 +1,256 @@
+package roundtripperreverserserver
+
+import (
+ "bytes"
+ "context"
+ "math"
+ "strings"
+ "testing"
+
+ "github.com/v2fly/struc"
+)
+
+func TestGenerateTokenLength(t *testing.T) {
+ p, err := NewPasswordAccessChecker("test-password")
+ if err != nil {
+ t.Fatalf("failed to create checker: %v", err)
+ }
+
+ tok, err := p.GenerateToken(12345)
+ if err != nil {
+ t.Fatalf("GenerateToken failed: %v", err)
+ }
+
+ if len(tok) != 16 {
+ t.Fatalf("expected token length 16, got %d", len(tok))
+ }
+}
+
+func TestGenerateAndCheckRoundTrip(t *testing.T) {
+ p, err := NewPasswordAccessChecker("another-secret")
+ if err != nil {
+ t.Fatalf("failed to create checker: %v", err)
+ }
+
+ userID := int64(42)
+ serverKey, err := p.GenerateToken(userID)
+ if err != nil {
+ t.Fatalf("GenerateToken failed: %v", err)
+ }
+
+ clientKey, err := p.CheckReverserAccess(context.Background(), serverKey)
+ if err != nil {
+ t.Fatalf("CheckReverserAccess failed: %v", err)
+ }
+
+ if len(clientKey) != 16 {
+ t.Fatalf("expected client key length 16, got %d", len(clientKey))
+ }
+
+ // decrypt clientKey using internal block to inspect contents
+ decrypted := make([]byte, 16)
+ p.block.Decrypt(decrypted, clientKey)
+
+ token := &tokenUnpacked{}
+ if err := struc.Unpack(bytes.NewReader(decrypted), token); err != nil {
+ t.Fatalf("failed to unpack client token: %v", err)
+ }
+
+ if token.Check != 0 {
+ t.Fatalf("expected check 0, got %d", token.Check)
+ }
+ if token.UserID != -userID {
+ t.Fatalf("expected userID %d, got %d", -userID, token.UserID)
+ }
+}
+
+func TestPasswordAccessChecker_ValidRoundTrip(t *testing.T) {
+ p, err := NewPasswordAccessChecker("test-password")
+ if err != nil {
+ t.Fatalf("NewPasswordAccessChecker failed: %v", err)
+ }
+
+ userID := int64(42)
+ token, err := p.GenerateToken(userID)
+ if err != nil {
+ t.Fatalf("GenerateToken failed: %v", err)
+ }
+
+ clientKey, err := p.CheckReverserAccess(context.TODO(), token)
+ if err != nil {
+ t.Fatalf("CheckReverserAccess failed: %v", err)
+ }
+
+ // decrypt clientKey and verify contents
+ buf := make([]byte, 16)
+ p.block.Decrypt(buf, clientKey)
+ tok := &tokenUnpacked{}
+ if err := struc.Unpack(bytes.NewReader(buf), tok); err != nil {
+ t.Fatalf("failed to unpack client token: %v", err)
+ }
+
+ if tok.Check != 0 {
+ t.Fatalf("unexpected Check value: got %d want 0", tok.Check)
+ }
+ if tok.UserID != -userID {
+ t.Fatalf("unexpected UserID: got %d want %d", tok.UserID, -userID)
+ }
+}
+
+func TestPasswordAccessChecker_InvalidKeyLength(t *testing.T) {
+ p, err := NewPasswordAccessChecker("test-password")
+ if err != nil {
+ t.Fatalf("NewPasswordAccessChecker failed: %v", err)
+ }
+
+ _, err = p.CheckReverserAccess(context.TODO(), []byte{1, 2, 3})
+ if err == nil {
+ t.Fatalf("expected error for invalid key length, got nil")
+ }
+ if !strings.Contains(err.Error(), "invalid server key length") {
+ t.Fatalf("unexpected error: %v", err)
+ }
+}
+
+func TestPasswordAccessChecker_InvalidTokenCheckValue(t *testing.T) {
+ p, err := NewPasswordAccessChecker("test-password")
+ if err != nil {
+ t.Fatalf("NewPasswordAccessChecker failed: %v", err)
+ }
+
+ // craft a token with non-zero Check
+ buf := bytes.NewBuffer(make([]byte, 0, 16))
+ if err := struc.Pack(buf, &tokenUnpacked{UserID: 7, Check: 1}); err != nil {
+ t.Fatalf("failed to pack token: %v", err)
+ }
+ plain := buf.Bytes()
+ if len(plain) != 16 {
+ t.Fatalf("unexpected packed length: %d", len(plain))
+ }
+ encrypted := make([]byte, 16)
+ p.block.Encrypt(encrypted, plain)
+
+ _, err = p.CheckReverserAccess(context.TODO(), encrypted)
+ if err == nil {
+ t.Fatalf("expected error for invalid token check value, got nil")
+ }
+ if !strings.Contains(err.Error(), "invalid token check value") {
+ t.Fatalf("unexpected error: %v", err)
+ }
+}
+
+func TestTokensNotInterchangeable(t *testing.T) {
+ p1, err := NewPasswordAccessChecker("password-one")
+ if err != nil {
+ t.Fatalf("failed to create checker p1: %v", err)
+ }
+ p2, err := NewPasswordAccessChecker("password-two")
+ if err != nil {
+ t.Fatalf("failed to create checker p2: %v", err)
+ }
+
+ userID := int64(7)
+ // token created by p1 should not be accepted by p2
+ tok1, err := p1.GenerateToken(userID)
+ if err != nil {
+ t.Fatalf("p1.GenerateToken failed: %v", err)
+ }
+ if _, err := p2.CheckReverserAccess(context.Background(), tok1); err == nil {
+ t.Fatalf("expected p2 to reject token generated by p1, but it accepted it")
+ }
+
+ // token created by p2 should not be accepted by p1
+ tok2, err := p2.GenerateToken(userID)
+ if err != nil {
+ t.Fatalf("p2.GenerateToken failed: %v", err)
+ }
+ if _, err := p1.CheckReverserAccess(context.Background(), tok2); err == nil {
+ t.Fatalf("expected p1 to reject token generated by p2, but it accepted it")
+ }
+}
+
+func TestCheckReverserAccess_NegativeUserID(t *testing.T) {
+ p, err := NewPasswordAccessChecker("neg-test")
+ if err != nil {
+ t.Fatalf("failed to create checker: %v", err)
+ }
+
+ // craft a token with negative UserID and Check == 0
+ buf := bytes.NewBuffer(make([]byte, 0, 16))
+ if err := struc.Pack(buf, &tokenUnpacked{UserID: -5, Check: 0}); err != nil {
+ t.Fatalf("failed to pack token: %v", err)
+ }
+ plain := buf.Bytes()
+ if len(plain) != 16 {
+ t.Fatalf("unexpected packed length: %d", len(plain))
+ }
+ serverKey := make([]byte, 16)
+ p.block.Encrypt(serverKey, plain)
+
+ if _, err := p.CheckReverserAccess(context.Background(), serverKey); err == nil {
+ t.Fatalf("expected error for negative userID token, got nil")
+ } else if !strings.Contains(err.Error(), "invalid token userID for server") {
+ t.Fatalf("unexpected error for negative userID token: %v", err)
+ }
+}
+
+func TestCheckReverserAccess_MinInt64Token(t *testing.T) {
+ p, err := NewPasswordAccessChecker("minint-test")
+ if err != nil {
+ t.Fatalf("failed to create checker: %v", err)
+ }
+
+ // craft a token with MinInt64 and Check == 0
+ buf := bytes.NewBuffer(make([]byte, 0, 16))
+ if err := struc.Pack(buf, &tokenUnpacked{UserID: math.MinInt64, Check: 0}); err != nil {
+ t.Fatalf("failed to pack token: %v", err)
+ }
+ plain := buf.Bytes()
+ if len(plain) != 16 {
+ t.Fatalf("unexpected packed length: %d", len(plain))
+ }
+ serverKey := make([]byte, 16)
+ p.block.Encrypt(serverKey, plain)
+
+ if _, err := p.CheckReverserAccess(context.Background(), serverKey); err == nil {
+ t.Fatalf("expected error for MinInt64 token, got nil")
+ } else if !strings.Contains(err.Error(), "invalid token userID: MinInt64") {
+ t.Fatalf("unexpected error for MinInt64 token: %v", err)
+ }
+}
+
+func TestGenerateTokensDifferentPasswords(t *testing.T) {
+ p1, err := NewPasswordAccessChecker("pw-a")
+ if err != nil {
+ t.Fatalf("failed to create checker p1: %v", err)
+ }
+ p2, err := NewPasswordAccessChecker("pw-b")
+ if err != nil {
+ t.Fatalf("failed to create checker p2: %v", err)
+ }
+
+ userID := int64(100)
+ tok1, err := p1.GenerateToken(userID)
+ if err != nil {
+ t.Fatalf("p1.GenerateToken failed: %v", err)
+ }
+ tok2, err := p2.GenerateToken(userID)
+ if err != nil {
+ t.Fatalf("p2.GenerateToken failed: %v", err)
+ }
+
+ if bytes.Equal(tok1, tok2) {
+ t.Fatalf("expected tokens from different passwords to differ, but they are equal")
+ }
+}
+
+func TestNewPasswordAccessChecker_EmptyPassword(t *testing.T) {
+ // ensure creating a checker with empty password succeeds and block is set
+ p, err := NewPasswordAccessChecker("")
+ if err != nil {
+ t.Fatalf("failed to create checker with empty password: %v", err)
+ }
+ if p.block == nil {
+ t.Fatalf("expected block to be initialized for empty password")
+ }
+}
diff --git a/v2ray-core/transport/internet/request/roundtripperreverserserver/clicommand/errors.generated.go b/v2ray-core/transport/internet/request/roundtripperreverserserver/clicommand/errors.generated.go
new file mode 100644
index 0000000000..32a4610799
--- /dev/null
+++ b/v2ray-core/transport/internet/request/roundtripperreverserserver/clicommand/errors.generated.go
@@ -0,0 +1,9 @@
+package clicommand
+
+import "github.com/v2fly/v2ray-core/v5/common/errors"
+
+type errPathObjHolder struct{}
+
+func newError(values ...interface{}) *errors.Error {
+ return errors.New(values...).WithPathObj(errPathObjHolder{})
+}
diff --git a/v2ray-core/transport/internet/request/roundtripperreverserserver/clicommand/generate_token_cli.go b/v2ray-core/transport/internet/request/roundtripperreverserserver/clicommand/generate_token_cli.go
new file mode 100644
index 0000000000..6953790e53
--- /dev/null
+++ b/v2ray-core/transport/internet/request/roundtripperreverserserver/clicommand/generate_token_cli.go
@@ -0,0 +1,98 @@
+package clicommand
+
+import (
+ "encoding/base64"
+ "flag"
+ "fmt"
+ "os"
+ "strconv"
+
+ "github.com/v2fly/v2ray-core/v5/main/commands/all/engineering"
+ "github.com/v2fly/v2ray-core/v5/main/commands/base"
+ "github.com/v2fly/v2ray-core/v5/transport/internet/request/roundtripperreverserserver"
+)
+
+var (
+ genPassword *string
+ genPasswordShort *string
+ genUser *string
+ genOut *string
+)
+
+var cmdGenerateToken = &base.Command{
+ UsageLine: "{{.Exec}} engineering request-rtt-reverser-gen-token",
+ Flag: func() flag.FlagSet {
+ fs := flag.NewFlagSet("", flag.ExitOnError)
+ // user-facing flag name: access passphrase
+ genPassword = fs.String("access-passphrase", "", "access passphrase used to derive token key (required)")
+ // short alias for convenience
+ genPasswordShort = fs.String("p", "", "access passphrase (shorthand)")
+ genUser = fs.String("u", "", "userID (required, int64, positive)")
+ genOut = fs.String("o", "", "write base64 tokens to file (optional)")
+ return *fs
+ }(),
+ Run: func(cmd *base.Command, args []string) {
+ if err := cmd.Flag.Parse(args); err != nil {
+ base.Fatalf("failed to parse flags: %v", err)
+ }
+
+ // require password (either long form or short alias)
+ var passphrase string
+ if genPasswordShort != nil && *genPasswordShort != "" {
+ passphrase = *genPasswordShort
+ } else if genPassword != nil && *genPassword != "" {
+ passphrase = *genPassword
+ } else {
+ base.Fatalf("-access-passphrase (or -p) is required")
+ }
+
+ if *genUser == "" {
+ base.Fatalf("-u userID is required")
+ }
+ id, err := strconv.ParseInt(*genUser, 10, 64)
+ if err != nil {
+ base.Fatalf("invalid userID %q: %v", *genUser, err)
+ }
+
+ // require positive userID
+ if id <= 0 {
+ base.Fatalf("userID must be a positive integer")
+ }
+
+ checker, err := roundtripperreverserserver.NewPasswordAccessChecker(passphrase)
+ if err != nil {
+ base.Fatalf("failed to create PasswordAccessChecker: %v", err)
+ }
+
+ // generate private (positive) and public (negative) tokens
+ privToken, err := checker.GenerateToken(id)
+ if err != nil {
+ base.Fatalf("GenerateToken (private) failed: %v", err)
+ }
+ pubToken, err := checker.GenerateToken(-id)
+ if err != nil {
+ base.Fatalf("GenerateToken (public) failed: %v", err)
+ }
+
+ b64Priv := base64.StdEncoding.EncodeToString(privToken)
+ b64Pub := base64.StdEncoding.EncodeToString(pubToken)
+
+ if *genOut != "" {
+ content := fmt.Sprintf("private: %s\npublic: %s\n", b64Priv, b64Pub)
+ if err := os.WriteFile(*genOut, []byte(content), 0o600); err != nil {
+ base.Fatalf("failed to write tokens to file %q: %v", *genOut, err)
+ }
+ if _, err := fmt.Fprintf(os.Stdout, "wrote base64 tokens to %s\n", *genOut); err != nil {
+ base.Fatalf("failed to write token message: %v", err)
+ }
+ return
+ }
+
+ // print both tokens to stdout
+ fmt.Printf("private: %s\npublic: %s\n", b64Priv, b64Pub)
+ },
+}
+
+func init() {
+ engineering.AddCommand(cmdGenerateToken)
+}
diff --git a/v2ray-core/transport/internet/request/roundtripperreverserserver/clicommand/roundTripperReserseServerCli.go b/v2ray-core/transport/internet/request/roundtripperreverserserver/clicommand/roundTripperReserseServerCli.go
new file mode 100644
index 0000000000..1c14fee852
--- /dev/null
+++ b/v2ray-core/transport/internet/request/roundtripperreverserserver/clicommand/roundTripperReserseServerCli.go
@@ -0,0 +1,65 @@
+package clicommand
+
+import (
+ "bytes"
+ "context"
+ "flag"
+ "os"
+
+ "github.com/v2fly/v2ray-core/v5/common/environment/envctx"
+ "github.com/v2fly/v2ray-core/v5/common/environment/systemnetworkimpl"
+ "github.com/v2fly/v2ray-core/v5/main/commands/all/engineering"
+ "github.com/v2fly/v2ray-core/v5/main/commands/base"
+ "github.com/v2fly/v2ray-core/v5/transport/internet/request/roundtripperreverserserver"
+
+ "google.golang.org/protobuf/encoding/protojson"
+)
+
+//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen
+
+var config *string
+
+var cmdRTTReverseServer = &base.Command{
+ UsageLine: "{{.Exec}} engineering request-rtt-reverse-server",
+ Flag: func() flag.FlagSet {
+ fs := flag.NewFlagSet("", flag.ExitOnError)
+ config = fs.String("c", "", "")
+ return *fs
+ }(),
+ Run: func(cmd *base.Command, args []string) {
+ err := cmd.Flag.Parse(args)
+ if err != nil {
+ base.Fatalf("failed to parse flags: %v", err)
+ }
+ fd, err := os.Open(*config)
+ if err != nil {
+ base.Fatalf("failed to open config file %q: %v", *config, err)
+ }
+ defer func() { _ = fd.Close() }()
+ content := bytes.NewBuffer(nil)
+ _, err = content.ReadFrom(fd)
+ if err != nil {
+ base.Fatalf("failed to read config file %q: %v", *config, err)
+ }
+ var reverserConfig roundtripperreverserserver.Config
+ err = protojson.Unmarshal(content.Bytes(), &reverserConfig)
+ if err != nil {
+ base.Fatalf("failed to unmarshal JSON config from %q: %v", *config, err)
+ }
+
+ ctx := context.Background()
+ systemNetworkImpl := systemnetworkimpl.NewSystemNetworkDefault()
+ ctx = envctx.ContextWithEnvironment(ctx, systemNetworkImpl)
+
+ server, err := roundtripperreverserserver.NewReverser(ctx, &reverserConfig)
+ if err != nil {
+ base.Fatalf("failed to create RTT reverse server: %v", err)
+ }
+ _ = server
+ select {}
+ },
+}
+
+func init() {
+ engineering.AddCommand(cmdRTTReverseServer)
+}
diff --git a/v2ray-core/transport/internet/request/roundtripperreverserserver/config.pb.go b/v2ray-core/transport/internet/request/roundtripperreverserserver/config.pb.go
new file mode 100644
index 0000000000..b7a9b83947
--- /dev/null
+++ b/v2ray-core/transport/internet/request/roundtripperreverserserver/config.pb.go
@@ -0,0 +1,140 @@
+package roundtripperreverserserver
+
+import (
+ _ "github.com/v2fly/v2ray-core/v5/common/protoext"
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ anypb "google.golang.org/protobuf/types/known/anypb"
+ reflect "reflect"
+ sync "sync"
+ unsafe "unsafe"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type Config struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ RoundTripperServer *anypb.Any `protobuf:"bytes,2,opt,name=round_tripper_server,json=roundTripperServer,proto3" json:"round_tripper_server,omitempty"`
+ Listen string `protobuf:"bytes,3,opt,name=listen,proto3" json:"listen,omitempty"`
+ AccessPassphrase string `protobuf:"bytes,4,opt,name=access_passphrase,json=accessPassphrase,proto3" json:"access_passphrase,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *Config) Reset() {
+ *x = Config{}
+ mi := &file_transport_internet_request_roundtripperreverserserver_config_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *Config) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Config) ProtoMessage() {}
+
+func (x *Config) ProtoReflect() protoreflect.Message {
+ mi := &file_transport_internet_request_roundtripperreverserserver_config_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Config.ProtoReflect.Descriptor instead.
+func (*Config) Descriptor() ([]byte, []int) {
+ return file_transport_internet_request_roundtripperreverserserver_config_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Config) GetRoundTripperServer() *anypb.Any {
+ if x != nil {
+ return x.RoundTripperServer
+ }
+ return nil
+}
+
+func (x *Config) GetListen() string {
+ if x != nil {
+ return x.Listen
+ }
+ return ""
+}
+
+func (x *Config) GetAccessPassphrase() string {
+ if x != nil {
+ return x.AccessPassphrase
+ }
+ return ""
+}
+
+var File_transport_internet_request_roundtripperreverserserver_config_proto protoreflect.FileDescriptor
+
+const file_transport_internet_request_roundtripperreverserserver_config_proto_rawDesc = "" +
+ "\n" +
+ "Btransport/internet/request/roundtripperreverserserver/config.proto\x12@v2ray.core.transport.internet.request.roundtripperreverserserver\x1a common/protoext/extensions.proto\x1a\x19google/protobuf/any.proto\"\xc9\x01\n" +
+ "\x06Config\x12F\n" +
+ "\x14round_tripper_server\x18\x02 \x01(\v2\x14.google.protobuf.AnyR\x12roundTripperServer\x12\x16\n" +
+ "\x06listen\x18\x03 \x01(\tR\x06listen\x12+\n" +
+ "\x11access_passphrase\x18\x04 \x01(\tR\x10accessPassphrase:2\x82\xb5\x18.\n" +
+ ",transport.request.roundtripperreverserserverB\xe0\x01\n" +
+ "Dcom.v2ray.core.transport.internet.request.roundtripperreverserserverP\x01ZTgithub.com/v2fly/v2ray-core/v5/transport/internet/request/roundtripperreverserserver\xaa\x02?V2Ray.Core.Transport.Internet.Request.RoundtripperReverseServerb\x06proto3"
+
+var (
+ file_transport_internet_request_roundtripperreverserserver_config_proto_rawDescOnce sync.Once
+ file_transport_internet_request_roundtripperreverserserver_config_proto_rawDescData []byte
+)
+
+func file_transport_internet_request_roundtripperreverserserver_config_proto_rawDescGZIP() []byte {
+ file_transport_internet_request_roundtripperreverserserver_config_proto_rawDescOnce.Do(func() {
+ file_transport_internet_request_roundtripperreverserserver_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_request_roundtripperreverserserver_config_proto_rawDesc), len(file_transport_internet_request_roundtripperreverserserver_config_proto_rawDesc)))
+ })
+ return file_transport_internet_request_roundtripperreverserserver_config_proto_rawDescData
+}
+
+var file_transport_internet_request_roundtripperreverserserver_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_transport_internet_request_roundtripperreverserserver_config_proto_goTypes = []any{
+ (*Config)(nil), // 0: v2ray.core.transport.internet.request.roundtripperreverserserver.Config
+ (*anypb.Any)(nil), // 1: google.protobuf.Any
+}
+var file_transport_internet_request_roundtripperreverserserver_config_proto_depIdxs = []int32{
+ 1, // 0: v2ray.core.transport.internet.request.roundtripperreverserserver.Config.round_tripper_server:type_name -> google.protobuf.Any
+ 1, // [1:1] is the sub-list for method output_type
+ 1, // [1:1] is the sub-list for method input_type
+ 1, // [1:1] is the sub-list for extension type_name
+ 1, // [1:1] is the sub-list for extension extendee
+ 0, // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_transport_internet_request_roundtripperreverserserver_config_proto_init() }
+func file_transport_internet_request_roundtripperreverserserver_config_proto_init() {
+ if File_transport_internet_request_roundtripperreverserserver_config_proto != nil {
+ return
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_request_roundtripperreverserserver_config_proto_rawDesc), len(file_transport_internet_request_roundtripperreverserserver_config_proto_rawDesc)),
+ NumEnums: 0,
+ NumMessages: 1,
+ NumExtensions: 0,
+ NumServices: 0,
+ },
+ GoTypes: file_transport_internet_request_roundtripperreverserserver_config_proto_goTypes,
+ DependencyIndexes: file_transport_internet_request_roundtripperreverserserver_config_proto_depIdxs,
+ MessageInfos: file_transport_internet_request_roundtripperreverserserver_config_proto_msgTypes,
+ }.Build()
+ File_transport_internet_request_roundtripperreverserserver_config_proto = out.File
+ file_transport_internet_request_roundtripperreverserserver_config_proto_goTypes = nil
+ file_transport_internet_request_roundtripperreverserserver_config_proto_depIdxs = nil
+}
diff --git a/v2ray-core/transport/internet/request/roundtripperreverserserver/config.proto b/v2ray-core/transport/internet/request/roundtripperreverserserver/config.proto
new file mode 100644
index 0000000000..78e194bbdb
--- /dev/null
+++ b/v2ray-core/transport/internet/request/roundtripperreverserserver/config.proto
@@ -0,0 +1,17 @@
+syntax = "proto3";
+
+package v2ray.core.transport.internet.request.roundtripperreverserserver;
+option csharp_namespace = "V2Ray.Core.Transport.Internet.Request.RoundtripperReverseServer";
+option go_package = "github.com/v2fly/v2ray-core/v5/transport/internet/request/roundtripperreverserserver";
+option java_package = "com.v2ray.core.transport.internet.request.roundtripperreverserserver";
+option java_multiple_files = true;
+
+import "common/protoext/extensions.proto";
+import "google/protobuf/any.proto";
+
+message Config {
+ option (v2ray.core.common.protoext.message_opt).type = "transport.request.roundtripperreverserserver";
+ google.protobuf.Any round_tripper_server = 2;
+ string listen = 3;
+ string access_passphrase = 4;
+}
\ No newline at end of file
diff --git a/v2ray-core/transport/internet/request/roundtripperreverserserver/errors.generated.go b/v2ray-core/transport/internet/request/roundtripperreverserserver/errors.generated.go
new file mode 100644
index 0000000000..b8ec9d0c82
--- /dev/null
+++ b/v2ray-core/transport/internet/request/roundtripperreverserserver/errors.generated.go
@@ -0,0 +1,9 @@
+package roundtripperreverserserver
+
+import "github.com/v2fly/v2ray-core/v5/common/errors"
+
+type errPathObjHolder struct{}
+
+func newError(values ...interface{}) *errors.Error {
+ return errors.New(values...).WithPathObj(errPathObjHolder{})
+}
diff --git a/v2ray-core/transport/internet/request/roundtripperreverserserver/reverser.go b/v2ray-core/transport/internet/request/roundtripperreverserserver/reverser.go
new file mode 100644
index 0000000000..6e6fccde34
--- /dev/null
+++ b/v2ray-core/transport/internet/request/roundtripperreverserserver/reverser.go
@@ -0,0 +1,127 @@
+package roundtripperreverserserver
+
+import (
+ "context"
+ "net"
+
+ "github.com/v2fly/v2ray-core/v5/common"
+ "github.com/v2fly/v2ray-core/v5/common/environment"
+ "github.com/v2fly/v2ray-core/v5/common/environment/envctx"
+ v2net "github.com/v2fly/v2ray-core/v5/common/net"
+ "github.com/v2fly/v2ray-core/v5/common/serial"
+ "github.com/v2fly/v2ray-core/v5/transport/internet/request"
+)
+
+//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen
+
+func NewReverser(ctx context.Context, config *Config) (*Reverser, error) {
+ reverser := &Reverser{
+ ctx: ctx,
+ config: config,
+ }
+ if err := reverser.init(); err != nil {
+ return nil, newError("failed to initialize Reverser").Base(err).AtError()
+ }
+ return reverser, nil
+}
+
+type Reverser struct {
+ ctx context.Context
+ config *Config
+ rttServer request.RoundTripperServer
+ reverser request.ReverserImpl
+ accessChecker request.ReverserAccessChecker
+}
+
+func (s *Reverser) OnRoundTrip(ctx context.Context, req request.Request, opts ...request.RoundTripperOption) (resp request.Response, err error) {
+ serverIntent := len(req.ConnectionTag) == 16
+ if serverIntent {
+ serverPublic, err := s.accessChecker.CheckReverserAccess(ctx, req.ConnectionTag)
+ if err != nil {
+ return request.Response{}, newError("reverser access check failed").Base(err).AtError()
+ }
+ reverserImpl, err := s.reverser.OnAuthenticatedServerIntentRoundTrip(ctx, serverPublic, req, opts...)
+ if err != nil {
+ return request.Response{}, newError("failed to handle authenticated server round trip").Base(err).AtError()
+ }
+ return reverserImpl, nil
+ }
+ if len(req.ConnectionTag) != 32 {
+ return request.Response{}, newError("invalid ConnectionTag length")
+ }
+ reverserImpl, err := s.reverser.OnOtherRoundTrip(ctx, req, opts...)
+ if err != nil {
+ return request.Response{}, newError("failed to handle client round trip").Base(err).AtError()
+ }
+ return reverserImpl, nil
+}
+
+func (s *Reverser) Listen(ctx context.Context) (v2net.Listener, error) {
+ systemNetworkCapabilitySet := envctx.EnvironmentFromContext(s.ctx).(environment.SystemNetworkCapabilitySet)
+ listener := systemNetworkCapabilitySet.Listener()
+ addr, err := v2net.ParseDestination(s.config.Listen)
+ if err != nil {
+ return nil, newError("invalid listen address " + s.config.Listen).Base(err).AtError()
+ }
+ netaddr := &net.TCPAddr{IP: addr.Address.IP(), Port: int(addr.Port)}
+ l, err := listener.Listen(ctx, netaddr, nil)
+ if err != nil {
+ return nil, newError("failed to listen on " + s.config.Listen).Base(err).AtError()
+ }
+ return l, nil
+}
+
+func (s *Reverser) init() error {
+ if s.config == nil {
+ return newError("nil ServerConfig")
+ }
+
+ if s.config.AccessPassphrase == "" {
+ return newError("empty AccessPassphrase in ServerConfig")
+ }
+ accessChecker, err := NewPasswordAccessChecker(s.config.AccessPassphrase)
+ if err != nil {
+ return newError("failed to create AccessChecker").Base(err).AtError()
+ }
+ s.accessChecker = accessChecker
+
+ ReverserImplInst, err := NewReverserImpl()
+ if err != nil {
+ return newError("failed to create ReverserImpl").Base(err).AtError()
+ }
+ s.reverser = ReverserImplInst
+
+ if s.config.RoundTripperServer == nil {
+ return newError("nil RoundTripperServer in ServerConfig")
+ }
+ RoundTripperServerConfig, err := serial.GetInstanceOf(s.config.RoundTripperServer)
+ if err != nil {
+ return newError("failed to get instance of RoundTripperServer").Base(err).AtError()
+ }
+ RoundTripperServerObj, err := common.CreateObject(s.ctx, RoundTripperServerConfig)
+ if err != nil {
+ return newError("failed to create RoundTripperServer").Base(err).AtError()
+ }
+ RoundTripperServerTyped, ok := RoundTripperServerObj.(request.RoundTripperServer)
+ if !ok {
+ return newError("RoundTripperServer is not a valid request.RoundTripperServer")
+ }
+ s.rttServer = RoundTripperServerTyped
+ s.rttServer.OnTransportServerAssemblyReady(s)
+ if err := s.rttServer.Start(); err != nil {
+ return newError("failed to start RoundTripperServer").Base(err).AtError()
+ }
+ return nil
+}
+
+func (s *Reverser) TripperReceiver() request.TripperReceiver {
+ return s
+}
+
+func (s *Reverser) SessionReceiver() request.SessionReceiver {
+ return nil
+}
+
+func (s *Reverser) AutoImplListener() request.Listener {
+ return s
+}
diff --git a/v2ray-core/transport/internet/request/roundtripperreverserserver/reverserimpl.go b/v2ray-core/transport/internet/request/roundtripperreverserserver/reverserimpl.go
new file mode 100644
index 0000000000..af09378ad7
--- /dev/null
+++ b/v2ray-core/transport/internet/request/roundtripperreverserserver/reverserimpl.go
@@ -0,0 +1,176 @@
+package roundtripperreverserserver
+
+import (
+ "context"
+ "sync"
+ "time"
+
+ "github.com/v2fly/v2ray-core/v5/common/task"
+ "github.com/v2fly/v2ray-core/v5/transport/internet/request"
+)
+
+func NewReverserImpl() (request.ReverserImpl, error) {
+ r := &ReverserImpl{
+ timeoutDuration: 5 * time.Minute,
+ }
+
+ // configure periodic cleaner
+ r.periodicCleaner = &task.Periodic{
+ Interval: time.Second * 30,
+ Execute: func() error {
+ now := time.Now()
+ r.serverPublicKeyToStateMap.Range(func(k, v interface{}) bool {
+ ss, ok := v.(*serverState)
+ if !ok {
+ return true
+ }
+ if now.Sub(ss.lastSeen) > r.timeoutDuration {
+ r.serverPublicKeyToStateMap.Delete(k)
+ r.serverPrivateKeyToPublicKey.Delete(ss.privateKey)
+ }
+ return true
+ })
+ return nil
+ },
+ }
+
+ // start the cleaner; return error if Start fails
+ if err := r.periodicCleaner.Start(); err != nil {
+ return nil, err
+ }
+
+ return r, nil
+}
+
+type ReverserImpl struct {
+ serverPublicKeyToStateMap sync.Map
+ serverPrivateKeyToPublicKey sync.Map
+
+ clientTemporaryKeyToStateMap sync.Map
+
+ // cleanup fields
+ periodicCleaner *task.Periodic
+ timeoutDuration time.Duration
+}
+
+// StopCleanup stops the periodic cleaner (if running).
+func (r *ReverserImpl) StopCleanup() error {
+ if r.periodicCleaner == nil {
+ return nil
+ }
+ return r.periodicCleaner.Close()
+}
+
+func (r *ReverserImpl) OnOtherRoundTrip(ctx context.Context, req request.Request, opts ...request.RoundTripperOption) (resp request.Response, err error) {
+ _ = ctx
+ _ = opts
+ routingKey := req.ConnectionTag
+ if len(routingKey) != 32 {
+ return request.Response{}, newError("invalid routing key")
+ }
+ sourceKey := routingKey[:16]
+ destKey := routingKey[16:]
+
+ if _, ok := r.serverPrivateKeyToPublicKey.Load(string(sourceKey)); ok {
+ stateInterface, clientOk := r.clientTemporaryKeyToStateMap.Load(string(destKey))
+ if clientOk {
+ state := stateInterface.(*clientState)
+ message := &reverserMessage{
+ Data: req.Data,
+ }
+ select {
+ case state.messageQueue <- message:
+ default:
+ return request.Response{}, newError("client message queue full")
+ }
+ return request.Response{}, nil
+ }
+ return request.Response{}, newError("no client found for the given routing key")
+ }
+
+ // try if this is a client to server message
+ stateInterface, _ := r.clientTemporaryKeyToStateMap.LoadOrStore(string(sourceKey), &clientState{
+ messageQueue: make(chan *reverserMessage, 1),
+ })
+ state := stateInterface.(*clientState)
+ defer func() {
+ r.clientTemporaryKeyToStateMap.Delete(string(sourceKey))
+ }()
+
+ serverStateInterface, ok := r.serverPublicKeyToStateMap.Load(string(destKey))
+ if ok {
+ serverStateInst := serverStateInterface.(*serverState)
+ message := &reverserMessage{
+ Data: req.Data,
+ }
+ timeOutTimer := time.NewTimer(time.Second * 25)
+ select {
+ case serverStateInst.messageQueue <- message:
+ case <-timeOutTimer.C:
+ return request.Response{}, newError("server message queue full timeout")
+ }
+
+ select {
+ case respMessage := <-state.messageQueue:
+ timeOutTimer.Stop()
+ return request.Response{
+ Data: respMessage.Data,
+ }, nil
+ case <-timeOutTimer.C:
+ return request.Response{}, newError("client message queue empty timeout")
+ }
+ }
+
+ return request.Response{}, newError("no server found for the given routing key")
+}
+
+func (r *ReverserImpl) OnAuthenticatedServerIntentRoundTrip(ctx context.Context, serverPublic []byte, req request.Request, opts ...request.RoundTripperOption) (resp request.Response, err error) {
+ _ = ctx
+ _ = opts
+ if len(req.ConnectionTag) != 16 {
+ return request.Response{}, newError("invalid server private key")
+ }
+
+ if len(serverPublic) != 16 {
+ return request.Response{}, newError("invalid server public key")
+ }
+
+ serverPrivate := req.ConnectionTag
+ // store mapping from private -> public
+ r.serverPrivateKeyToPublicKey.Store(string(serverPrivate), string(serverPublic))
+ stateInterface, _ := r.serverPublicKeyToStateMap.LoadOrStore(string(serverPublic), &serverState{
+ messageQueue: make(chan *reverserMessage, 16),
+ lastSeen: time.Now(),
+ })
+ state := stateInterface.(*serverState)
+ state.lastSeen = time.Now()
+ timeOutTimer := time.NewTimer(time.Minute)
+ select {
+ case message := <-state.messageQueue:
+ timeOutTimer.Stop()
+ return request.Response{
+ Data: message.Data,
+ }, nil
+ case <-timeOutTimer.C:
+ return request.Response{}, nil
+ }
+}
+
+func (r *ReverserImpl) __CleanupNow____TestOnly() {
+ // trigger cleanup immediately for testing purposes
+ r.periodicCleaner.Execute()
+}
+
+type clientState struct {
+ messageQueue chan *reverserMessage
+}
+
+type serverState struct {
+ messageQueue chan *reverserMessage
+ lastSeen time.Time
+ privateKey string
+}
+
+type reverserMessage struct {
+ Data []byte
+}
diff --git a/v2ray-core/transport/internet/request/roundtripperreverserserver/reverserimpl_test.go b/v2ray-core/transport/internet/request/roundtripperreverserserver/reverserimpl_test.go
new file mode 100644
index 0000000000..51f69b29ea
--- /dev/null
+++ b/v2ray-core/transport/internet/request/roundtripperreverserserver/reverserimpl_test.go
@@ -0,0 +1,287 @@
+package roundtripperreverserserver
+
+import (
+ "context"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/v2fly/v2ray-core/v5/transport/internet/request"
+)
+
+// helper to make a byte slice of given length filled with a pattern
+func fill(b byte, n int) []byte {
+ res := make([]byte, n)
+ for i := range res {
+ res[i] = b
+ }
+ return res
+}
+
+// stopCleanup tries to call StopCleanup on concrete *ReverserImpl returned by NewReverserImpl.
+func stopCleanup(t *testing.T, r request.ReverserImpl) {
+ if r == nil {
+ return
+ }
+ if impl, ok := r.(*ReverserImpl); ok {
+ if err := impl.StopCleanup(); err != nil {
+ t.Fatalf("StopCleanup failed: %v", err)
+ }
+ } else {
+ t.Fatalf("expected *ReverserImpl, got %T", r)
+ }
+}
+
+func TestOnOtherRoundTrip_InvalidRoutingKeyLength(t *testing.T) {
+ r, err := NewReverserImpl()
+ if err != nil {
+ // constructor currently never errors
+ t.Fatalf("unexpected constructor error: %v", err)
+ }
+ defer stopCleanup(t, r)
+ _, gotErr := r.OnOtherRoundTrip(context.Background(), request.Request{ConnectionTag: []byte("short")})
+ if gotErr == nil {
+ t.Fatalf("expected error for invalid routing key length")
+ }
+ if !strings.Contains(gotErr.Error(), "invalid routing key") {
+ t.Fatalf("unexpected error: %v", gotErr)
+ }
+}
+
+func TestOnOtherRoundTrip_ServerToClient_NoClientFound(t *testing.T) {
+ r, _ := NewReverserImpl()
+ defer stopCleanup(t, r)
+ impl := r.(*ReverserImpl)
+ source := fill('A', 16)
+ dest := fill('B', 16)
+ impl.serverPrivateKeyToPublicKey.Store(string(source), string(source))
+ _, err := impl.OnOtherRoundTrip(context.Background(), request.Request{ConnectionTag: append(source, dest...)})
+ if err == nil || !strings.Contains(err.Error(), "no client found") {
+ t.Fatalf("expected no client found error, got: %v", err)
+ }
+}
+
+func TestOnOtherRoundTrip_ServerToClient_Success(t *testing.T) {
+ r, _ := NewReverserImpl()
+ defer stopCleanup(t, r)
+ impl := r.(*ReverserImpl)
+ source := fill('S', 16)
+ dest := fill('C', 16)
+ impl.serverPrivateKeyToPublicKey.Store(string(source), string(source))
+ clientState := &clientState{messageQueue: make(chan *reverserMessage, 1)}
+ impl.clientTemporaryKeyToStateMap.Store(string(dest), clientState)
+ data := []byte("hello-client")
+ _, err := impl.OnOtherRoundTrip(context.Background(), request.Request{ConnectionTag: append(source, dest...), Data: data})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ select {
+ case msg := <-clientState.messageQueue:
+ if string(msg.Data) != string(data) {
+ t.Fatalf("unexpected message data: got %q want %q", msg.Data, data)
+ }
+ default:
+ t.Fatalf("expected message queued for client but queue empty")
+ }
+}
+
+func TestOnOtherRoundTrip_ServerToClient_QueueFull(t *testing.T) {
+ r, _ := NewReverserImpl()
+ defer stopCleanup(t, r)
+ impl := r.(*ReverserImpl)
+ source := fill('F', 16)
+ dest := fill('Q', 16)
+ impl.serverPrivateKeyToPublicKey.Store(string(source), string(source))
+ clientState := &clientState{messageQueue: make(chan *reverserMessage, 1)}
+ // pre-fill queue so next send fails non-blocking
+ clientState.messageQueue <- &reverserMessage{Data: []byte("prefill")}
+ impl.clientTemporaryKeyToStateMap.Store(string(dest), clientState)
+ _, err := impl.OnOtherRoundTrip(context.Background(), request.Request{ConnectionTag: append(source, dest...), Data: []byte("new")})
+ if err == nil || !strings.Contains(err.Error(), "client message queue full") {
+ t.Fatalf("expected queue full error, got: %v", err)
+ }
+}
+
+func TestOnOtherRoundTrip_ClientToServer_NoServerFound(t *testing.T) {
+ r, _ := NewReverserImpl()
+ defer stopCleanup(t, r)
+ impl := r.(*ReverserImpl)
+ source := fill('X', 16)
+ dest := fill('Y', 16)
+ _, err := impl.OnOtherRoundTrip(context.Background(), request.Request{ConnectionTag: append(source, dest...), Data: []byte("ping")})
+ if err == nil || !strings.Contains(err.Error(), "no server found") {
+ t.Fatalf("expected no server found error, got: %v", err)
+ }
+}
+
+func TestOnOtherRoundTrip_ClientToServer_Success(t *testing.T) {
+ r, _ := NewReverserImpl()
+ defer stopCleanup(t, r)
+ impl := r.(*ReverserImpl)
+ source := fill('1', 16)
+ dest := fill('2', 16)
+ serverState := &serverState{messageQueue: make(chan *reverserMessage, 1)}
+ impl.serverPublicKeyToStateMap.Store(string(dest), serverState)
+ clientState := &clientState{messageQueue: make(chan *reverserMessage, 1)}
+ clientState.messageQueue <- &reverserMessage{Data: []byte("pong")}
+ impl.clientTemporaryKeyToStateMap.Store(string(source), clientState)
+ data := []byte("ping")
+ resp, err := impl.OnOtherRoundTrip(context.Background(), request.Request{ConnectionTag: append(source, dest...), Data: data})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ // verify server received the original message
+ select {
+ case msg := <-serverState.messageQueue:
+ if string(msg.Data) != string(data) {
+ t.Fatalf("server message mismatch: got %q want %q", msg.Data, data)
+ }
+ default:
+ t.Fatalf("expected server to receive message but queue empty")
+ }
+ if string(resp.Data) != "pong" {
+ t.Fatalf("unexpected response data: got %q want %q", resp.Data, "pong")
+ }
+}
+
+func TestOnAuthenticatedServerIntentRoundTrip_InvalidPrivateKeyLength(t *testing.T) {
+ r, _ := NewReverserImpl()
+ defer stopCleanup(t, r)
+ impl := r.(*ReverserImpl)
+ serverPublic := fill('P', 16)
+ _, err := impl.OnAuthenticatedServerIntentRoundTrip(context.Background(), serverPublic, request.Request{ConnectionTag: fill('Z', 15)})
+ if err == nil || !strings.Contains(err.Error(), "invalid server private key") {
+ t.Fatalf("expected invalid server private key error, got: %v", err)
+ }
+}
+
+func TestOnAuthenticatedServerIntentRoundTrip_InvalidPublicKeyLength(t *testing.T) {
+ r, _ := NewReverserImpl()
+ defer stopCleanup(t, r)
+ impl := r.(*ReverserImpl)
+ _, err := impl.OnAuthenticatedServerIntentRoundTrip(context.Background(), fill('P', 15), request.Request{ConnectionTag: fill('K', 16)})
+ if err == nil || !strings.Contains(err.Error(), "invalid server public key") {
+ t.Fatalf("expected invalid server public key error, got: %v", err)
+ }
+}
+
+func TestOnAuthenticatedServerIntentRoundTrip_SuccessMessage(t *testing.T) {
+ r, _ := NewReverserImpl()
+ defer stopCleanup(t, r)
+ impl := r.(*ReverserImpl)
+ serverPublic := fill('P', 16)
+ serverPrivate := fill('K', 16)
+ state := &serverState{messageQueue: make(chan *reverserMessage, 1)}
+ state.messageQueue <- &reverserMessage{Data: []byte("welcome")}
+ impl.serverPublicKeyToStateMap.Store(string(serverPublic), state)
+ resp, err := impl.OnAuthenticatedServerIntentRoundTrip(context.Background(), serverPublic, request.Request{ConnectionTag: serverPrivate})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if string(resp.Data) != "welcome" {
+ t.Fatalf("unexpected response data: got %q want %q", resp.Data, "welcome")
+ }
+ if _, ok := impl.serverPrivateKeyToPublicKey.Load(string(serverPrivate)); !ok {
+ t.Fatalf("expected server private key mapping to be stored")
+ }
+}
+
+func TestSmoke(t *testing.T) {
+ if _, err := NewReverserImpl(); err != nil {
+ t.Fatalf("unexpected error constructing reverser: %v", err)
+ }
+}
+
+func TestPeriodicCleanupRemovesOldServer(t *testing.T) {
+ r, _ := NewReverserImpl()
+ defer stopCleanup(t, r)
+ impl := r.(*ReverserImpl)
+
+ // configure short durations for test speed
+ impl.timeoutDuration = 10 * time.Millisecond
+ impl.periodicCleaner.Interval = 5 * time.Millisecond
+
+ // insert a server state with lastSeen far in the past
+ old := &serverState{messageQueue: make(chan *reverserMessage, 1), lastSeen: time.Now().Add(-time.Hour)}
+ impl.serverPublicKeyToStateMap.Store("old-server", old)
+
+ // trigger cleanup now
+ impl.__CleanupNow____TestOnly()
+
+ if _, ok := impl.serverPublicKeyToStateMap.Load("old-server"); ok {
+ t.Fatalf("expected old server entry to be removed by cleaner")
+ }
+}
+
+func TestCleanupNowRemovesServerAndPrivateMapping(t *testing.T) {
+ r, _ := NewReverserImpl()
+ defer stopCleanup(t, r)
+ impl := r.(*ReverserImpl)
+
+ // create an old server entry with private key present in private->public map
+ publicKey := "old-server-public"
+ privateKey := "old-server-private"
+ old := &serverState{messageQueue: make(chan *reverserMessage, 1), lastSeen: time.Now().Add(-time.Hour), privateKey: privateKey}
+ impl.serverPublicKeyToStateMap.Store(publicKey, old)
+ impl.serverPrivateKeyToPublicKey.Store(privateKey, publicKey)
+
+ // trigger immediate cleanup
+ impl.__CleanupNow____TestOnly()
+
+ if _, ok := impl.serverPublicKeyToStateMap.Load(publicKey); ok {
+ t.Fatalf("expected old server entry to be removed by cleaner via CleanupNow")
+ }
+ if _, ok := impl.serverPrivateKeyToPublicKey.Load(privateKey); ok {
+ t.Fatalf("expected old private->public mapping to be removed by cleaner via CleanupNow")
+ }
+}
+
+func TestStopCleanupIdempotent(t *testing.T) {
+ r, _ := NewReverserImpl()
+ impl := r.(*ReverserImpl)
+ // first stop
+ if err := impl.StopCleanup(); err != nil {
+ t.Fatalf("StopCleanup failed: %v", err)
+ }
+ // second stop should not panic or return error
+ if err := impl.StopCleanup(); err != nil {
+ t.Fatalf("StopCleanup second call failed: %v", err)
+ }
+}
+
+func TestOnAuthenticatedServerIntentRoundTrip_UpdatesLastSeenAndMapping(t *testing.T) {
+ r, _ := NewReverserImpl()
+ defer stopCleanup(t, r)
+ impl := r.(*ReverserImpl)
+
+ serverPublic := fill('P', 16)
+ serverPrivate := fill('K', 16)
+
+ // pre-insert a server state that has an old lastSeen but contains a queued message
+ state := &serverState{messageQueue: make(chan *reverserMessage, 1), lastSeen: time.Now().Add(-time.Hour)}
+ state.messageQueue <- &reverserMessage{Data: []byte("welcome-back")}
+ impl.serverPublicKeyToStateMap.Store(string(serverPublic), state)
+
+ resp, err := impl.OnAuthenticatedServerIntentRoundTrip(context.Background(), serverPublic, request.Request{ConnectionTag: serverPrivate})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if string(resp.Data) != "welcome-back" {
+ t.Fatalf("unexpected response data: got %q want %q", resp.Data, "welcome-back")
+ }
+
+ // mapping from private -> public should be stored
+ if v, ok := impl.serverPrivateKeyToPublicKey.Load(string(serverPrivate)); !ok || v != string(serverPublic) {
+ t.Fatalf("expected private->public mapping stored, got %v (ok=%v)", v, ok)
+ }
+
+ // lastSeen should be updated to a recent time
+ si, ok := impl.serverPublicKeyToStateMap.Load(string(serverPublic))
+ if !ok {
+ t.Fatalf("expected server state to still exist after intent call")
+ }
+ ss := si.(*serverState)
+ if time.Since(ss.lastSeen) > time.Second*2 {
+ t.Fatalf("expected lastSeen to be updated recently, got %v", ss.lastSeen)
+ }
+}
diff --git a/v2ray-core/transport/internet/tlsmirror/data.pb.go b/v2ray-core/transport/internet/tlsmirror/data.pb.go
index 0dd21f4184..2e25c85816 100644
--- a/v2ray-core/transport/internet/tlsmirror/data.pb.go
+++ b/v2ray-core/transport/internet/tlsmirror/data.pb.go
@@ -20,7 +20,8 @@ type EnrollmentConfirmationReq struct {
ServerIdentifier []byte `protobuf:"bytes,1,opt,name=server_identifier,json=serverIdentifier,proto3" json:"server_identifier,omitempty"`
CarrierTlsConnectionClientRandom []byte `protobuf:"bytes,2,opt,name=carrier_tls_connection_client_random,json=carrierTlsConnectionClientRandom,proto3" json:"carrier_tls_connection_client_random,omitempty"`
CarrierTlsConnectionServerRandom []byte `protobuf:"bytes,3,opt,name=carrier_tls_connection_server_random,json=carrierTlsConnectionServerRandom,proto3" json:"carrier_tls_connection_server_random,omitempty"`
- IsSelfEnrollment bool `protobuf:"varint,4,opt,name=is_self_enrollment,json=isSelfEnrollment,proto3" json:"is_self_enrollment,omitempty"`
+ ClientIdentifier []byte `protobuf:"bytes,4,opt,name=client_identifier,json=clientIdentifier,proto3" json:"client_identifier,omitempty"` // Implicated
+ ReplyAddressTag []byte `protobuf:"bytes,5,opt,name=reply_address_tag,json=replyAddressTag,proto3" json:"reply_address_tag,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -76,11 +77,18 @@ func (x *EnrollmentConfirmationReq) GetCarrierTlsConnectionServerRandom() []byte
return nil
}
-func (x *EnrollmentConfirmationReq) GetIsSelfEnrollment() bool {
+func (x *EnrollmentConfirmationReq) GetClientIdentifier() []byte {
if x != nil {
- return x.IsSelfEnrollment
+ return x.ClientIdentifier
}
- return false
+ return nil
+}
+
+func (x *EnrollmentConfirmationReq) GetReplyAddressTag() []byte {
+ if x != nil {
+ return x.ReplyAddressTag
+ }
+ return nil
}
type EnrollmentConfirmationResp struct {
@@ -131,12 +139,13 @@ var File_transport_internet_tlsmirror_data_proto protoreflect.FileDescriptor
const file_transport_internet_tlsmirror_data_proto_rawDesc = "" +
"\n" +
- "'transport/internet/tlsmirror/data.proto\x12'v2ray.core.transport.internet.tlsmirror\"\x96\x02\n" +
+ "'transport/internet/tlsmirror/data.proto\x12'v2ray.core.transport.internet.tlsmirror\"\xc1\x02\n" +
"\x19EnrollmentConfirmationReq\x12+\n" +
"\x11server_identifier\x18\x01 \x01(\fR\x10serverIdentifier\x12N\n" +
"$carrier_tls_connection_client_random\x18\x02 \x01(\fR carrierTlsConnectionClientRandom\x12N\n" +
- "$carrier_tls_connection_server_random\x18\x03 \x01(\fR carrierTlsConnectionServerRandom\x12,\n" +
- "\x12is_self_enrollment\x18\x04 \x01(\bR\x10isSelfEnrollment\"8\n" +
+ "$carrier_tls_connection_server_random\x18\x03 \x01(\fR carrierTlsConnectionServerRandom\x12+\n" +
+ "\x11client_identifier\x18\x04 \x01(\fR\x10clientIdentifier\x12*\n" +
+ "\x11reply_address_tag\x18\x05 \x01(\fR\x0freplyAddressTag\"8\n" +
"\x1aEnrollmentConfirmationResp\x12\x1a\n" +
"\benrolled\x18\x01 \x01(\bR\benrolledB\x96\x01\n" +
"+com.v2ray.core.transport.internet.tlsmirrorP\x01Z;github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror\xaa\x02'V2Ray.Core.Transport.Internet.Tlsmirrorb\x06proto3"
diff --git a/v2ray-core/transport/internet/tlsmirror/data.proto b/v2ray-core/transport/internet/tlsmirror/data.proto
index c3873a571b..6c269efb72 100644
--- a/v2ray-core/transport/internet/tlsmirror/data.proto
+++ b/v2ray-core/transport/internet/tlsmirror/data.proto
@@ -11,7 +11,8 @@ message EnrollmentConfirmationReq{
bytes server_identifier = 1;
bytes carrier_tls_connection_client_random = 2;
bytes carrier_tls_connection_server_random = 3;
- bool is_self_enrollment = 4;
+ bytes client_identifier = 4; // Implicated
+ bytes reply_address_tag = 5;
}
message EnrollmentConfirmationResp{
bool enrolled = 1;
diff --git a/v2ray-core/transport/internet/tlsmirror/mirrorcrypto/derive_key.go b/v2ray-core/transport/internet/tlsmirror/mirrorcrypto/derive_key.go
index b941998b70..d28099809b 100644
--- a/v2ray-core/transport/internet/tlsmirror/mirrorcrypto/derive_key.go
+++ b/v2ray-core/transport/internet/tlsmirror/mirrorcrypto/derive_key.go
@@ -62,12 +62,12 @@ func DeriveSequenceWatermarkingKey(primaryKey, clientRandom, serverRandom []byte
combined := append(primaryKey, clientRandom...) // nolint: gocritic
combined = append(combined, serverRandom...)
- encryptionKey, err := hkdf.Expand(sha256.New, combined, "v2ray-xv64FXUU-GxMn8UYz-bTy6UDeE:tlsmirror-sequence-watermark"+tag, 32)
+ encryptionKey, err := hkdf.Expand(sha256.New, combined, "v2ray-xv64FXUU-GxMn8UYz-bTy6UDeE:tlsmirror-sequence-watermark-encryption"+tag, 32)
if err != nil {
return nil, nil, newError("unable to derive encryption key").Base(err)
}
- nonceMask, err := hkdf.Expand(sha256.New, combined, "v2ray-xv64FXUU-GxMn8UYz-bTy6UDeE:tlsmirror-sequence-watermark"+tag, 24)
+ nonceMask, err := hkdf.Expand(sha256.New, combined, "v2ray-xv64FXUU-GxMn8UYz-bTy6UDeE:tlsmirror-sequence-watermark-noncemask"+tag, 24)
if err != nil {
return nil, nil, newError("unable to derive nonce mask").Base(err)
}
diff --git a/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/cancelContextOnCloseConn.go b/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/cancelContextOnCloseConn.go
new file mode 100644
index 0000000000..6813ef19a7
--- /dev/null
+++ b/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/cancelContextOnCloseConn.go
@@ -0,0 +1,23 @@
+package mirrorenrollment
+
+import (
+ "context"
+ "net"
+)
+
+func NewCancelContextOnCloseConn(conn net.Conn, done context.CancelFunc) net.Conn {
+ return &cancelContextOnCloseConn{
+ Conn: conn,
+ done: done,
+ }
+}
+
+type cancelContextOnCloseConn struct {
+ net.Conn
+ done context.CancelFunc
+}
+
+func (c *cancelContextOnCloseConn) Close() error {
+ c.done()
+ return c.Conn.Close()
+}
diff --git a/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/client.go b/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/client.go
index 4daa86f760..975ef5cbf4 100644
--- a/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/client.go
+++ b/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/client.go
@@ -98,7 +98,14 @@ func (c *EnrollmentConfirmationClient) init() error {
primaryConfirmationOutbound = transportEnvironment.SelfProxyTag()
loopbackProtectedCtx = mirrorcommon.SetSecondaryLoopbackProtectionFlagForContext(c.ctx, c.serverIdentity)
}
- return dialer(loopbackProtectedCtx, dest, primaryConfirmationOutbound)
+ connContext, done := context.WithCancel(loopbackProtectedCtx)
+ conn, err := dialer(connContext, dest, primaryConfirmationOutbound)
+ if err != nil {
+ done()
+ return nil, newError("failed to dial to destination").Base(err).AtError()
+ }
+ contextLinkedConn := NewCancelContextOnCloseConn(conn, done)
+ return contextLinkedConn, nil
}, c.serverIdentity)
if err != nil {
return newError("failed to create HTTP round tripper for enrollment confirmation").Base(err).AtError()
diff --git a/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/httpenrollmentconfirmation/clientbuilder.go b/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/httpenrollmentconfirmation/clientbuilder.go
index ec7f025146..fb722be684 100644
--- a/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/httpenrollmentconfirmation/clientbuilder.go
+++ b/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/httpenrollmentconfirmation/clientbuilder.go
@@ -1,10 +1,12 @@
package httpenrollmentconfirmation
import (
+ "context"
"encoding/base32"
"net"
"net/http"
"sync"
+ "time"
"github.com/v2fly/v2ray-core/v5/common"
"github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror"
@@ -81,9 +83,16 @@ func (c *clientRoundtripper) roundTrip(request *http.Request) (*http.Response, e
}
defer c.currentConnLock.RUnlock()
- // Use the current connection to perform the round trip
+ timeoutContext, _ := context.WithTimeout(context.Background(), time.Second*30) //nolint:govet
+ request = request.WithContext(timeoutContext)
+
resp, err := c.currentConn.RoundTrip(request)
+ // Use the current connection to perform the round trip
if err != nil {
+ if resp != nil && resp.Body != nil {
+ resp.Body.Close()
+ }
+
defer func() {
c.currentConnLock.RUnlock()
c.currentConnLock.Lock()
@@ -116,7 +125,7 @@ func (c *clientRoundtripper) createNewConnection() error {
c.currentConnInnerConn = conn
c.currentConn, err = httponconnection.NewSingleConnectionHTTPTransport(conn, "h2")
if err != nil {
- conn.Close() // Close the connection if transport creation fails
+ _ = conn.Close() // Close the connection if transport creation fails
return newError("failed to create HTTP transport: ", err)
}
return nil
diff --git a/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/client.go b/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/client.go
index e833a52a57..24fac65dc8 100644
--- a/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/client.go
+++ b/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/client.go
@@ -3,6 +3,7 @@ package roundtripperenrollmentconfirmation
import (
"context"
csrand "crypto/rand"
+ "io"
"net"
"google.golang.org/protobuf/proto"
@@ -118,7 +119,15 @@ func (c *Client) VerifyConnectionEnrollment(req *tlsmirror.EnrollmentConfirmatio
if c.config.ServerIdentity != nil {
connectionTagServerID = c.config.ServerIdentity
}
- connectionTag := append(connectionTagServerID, c.clientTemporaryIdentifier...) //nolint:gocritic
+ var replyAddressTag [16]byte
+ _, err := io.ReadFull(csrand.Reader, replyAddressTag[:])
+ if err != nil {
+ return nil, newError("failed to generate reply address tag").Base(err)
+ }
+
+ connectionTag := append(replyAddressTag[:], connectionTagServerID...) //nolint:gocritic
+ req.ClientIdentifier = c.clientTemporaryIdentifier
+ req.ReplyAddressTag = replyAddressTag[:]
wrappedData, err := proto.Marshal(req)
if err != nil {
return nil, newError("failed to marshal enrollment confirmation request").Base(err)
diff --git a/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/config.pb.go b/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/config.pb.go
index 1aec9a4ee6..defc426b7a 100644
--- a/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/config.pb.go
+++ b/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/config.pb.go
@@ -93,6 +93,82 @@ func (x *ClientConfig) GetServerIdentity() []byte {
return nil
}
+type ServerInverseRoleConfig struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ RoundTripperClient *anypb.Any `protobuf:"bytes,1,opt,name=round_tripper_client,json=roundTripperClient,proto3" json:"round_tripper_client,omitempty"`
+ SecurityConfig *anypb.Any `protobuf:"bytes,2,opt,name=security_config,json=securityConfig,proto3" json:"security_config,omitempty"`
+ Dest string `protobuf:"bytes,3,opt,name=dest,proto3" json:"dest,omitempty"`
+ OutboundTag string `protobuf:"bytes,4,opt,name=outbound_tag,json=outboundTag,proto3" json:"outbound_tag,omitempty"`
+ ServerIdentity []byte `protobuf:"bytes,5,opt,name=server_identity,json=serverIdentity,proto3" json:"server_identity,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ServerInverseRoleConfig) Reset() {
+ *x = ServerInverseRoleConfig{}
+ mi := &file_transport_internet_tlsmirror_mirrorenrollment_roundtripperenrollmentconfirmation_config_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ServerInverseRoleConfig) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ServerInverseRoleConfig) ProtoMessage() {}
+
+func (x *ServerInverseRoleConfig) ProtoReflect() protoreflect.Message {
+ mi := &file_transport_internet_tlsmirror_mirrorenrollment_roundtripperenrollmentconfirmation_config_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ServerInverseRoleConfig.ProtoReflect.Descriptor instead.
+func (*ServerInverseRoleConfig) Descriptor() ([]byte, []int) {
+ return file_transport_internet_tlsmirror_mirrorenrollment_roundtripperenrollmentconfirmation_config_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *ServerInverseRoleConfig) GetRoundTripperClient() *anypb.Any {
+ if x != nil {
+ return x.RoundTripperClient
+ }
+ return nil
+}
+
+func (x *ServerInverseRoleConfig) GetSecurityConfig() *anypb.Any {
+ if x != nil {
+ return x.SecurityConfig
+ }
+ return nil
+}
+
+func (x *ServerInverseRoleConfig) GetDest() string {
+ if x != nil {
+ return x.Dest
+ }
+ return ""
+}
+
+func (x *ServerInverseRoleConfig) GetOutboundTag() string {
+ if x != nil {
+ return x.OutboundTag
+ }
+ return ""
+}
+
+func (x *ServerInverseRoleConfig) GetServerIdentity() []byte {
+ if x != nil {
+ return x.ServerIdentity
+ }
+ return nil
+}
+
type ServerConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
RoundTripperServer *anypb.Any `protobuf:"bytes,2,opt,name=round_tripper_server,json=roundTripperServer,proto3" json:"round_tripper_server,omitempty"`
@@ -103,7 +179,7 @@ type ServerConfig struct {
func (x *ServerConfig) Reset() {
*x = ServerConfig{}
- mi := &file_transport_internet_tlsmirror_mirrorenrollment_roundtripperenrollmentconfirmation_config_proto_msgTypes[1]
+ mi := &file_transport_internet_tlsmirror_mirrorenrollment_roundtripperenrollmentconfirmation_config_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -115,7 +191,7 @@ func (x *ServerConfig) String() string {
func (*ServerConfig) ProtoMessage() {}
func (x *ServerConfig) ProtoReflect() protoreflect.Message {
- mi := &file_transport_internet_tlsmirror_mirrorenrollment_roundtripperenrollmentconfirmation_config_proto_msgTypes[1]
+ mi := &file_transport_internet_tlsmirror_mirrorenrollment_roundtripperenrollmentconfirmation_config_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -128,7 +204,7 @@ func (x *ServerConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use ServerConfig.ProtoReflect.Descriptor instead.
func (*ServerConfig) Descriptor() ([]byte, []int) {
- return file_transport_internet_tlsmirror_mirrorenrollment_roundtripperenrollmentconfirmation_config_proto_rawDescGZIP(), []int{1}
+ return file_transport_internet_tlsmirror_mirrorenrollment_roundtripperenrollmentconfirmation_config_proto_rawDescGZIP(), []int{2}
}
func (x *ServerConfig) GetRoundTripperServer() *anypb.Any {
@@ -155,6 +231,12 @@ const file_transport_internet_tlsmirror_mirrorenrollment_roundtripperenrollmentc
"\x0fsecurity_config\x18\x02 \x01(\v2\x14.google.protobuf.AnyR\x0esecurityConfig\x12\x12\n" +
"\x04dest\x18\x03 \x01(\tR\x04dest\x12!\n" +
"\foutbound_tag\x18\x04 \x01(\tR\voutboundTag\x12'\n" +
+ "\x0fserver_identity\x18\x05 \x01(\fR\x0eserverIdentity\"\x80\x02\n" +
+ "\x17ServerInverseRoleConfig\x12F\n" +
+ "\x14round_tripper_client\x18\x01 \x01(\v2\x14.google.protobuf.AnyR\x12roundTripperClient\x12=\n" +
+ "\x0fsecurity_config\x18\x02 \x01(\v2\x14.google.protobuf.AnyR\x0esecurityConfig\x12\x12\n" +
+ "\x04dest\x18\x03 \x01(\tR\x04dest\x12!\n" +
+ "\foutbound_tag\x18\x04 \x01(\tR\voutboundTag\x12'\n" +
"\x0fserver_identity\x18\x05 \x01(\fR\x0eserverIdentity\"n\n" +
"\fServerConfig\x12F\n" +
"\x14round_tripper_server\x18\x02 \x01(\v2\x14.google.protobuf.AnyR\x12roundTripperServer\x12\x16\n" +
@@ -173,21 +255,24 @@ func file_transport_internet_tlsmirror_mirrorenrollment_roundtripperenrollmentco
return file_transport_internet_tlsmirror_mirrorenrollment_roundtripperenrollmentconfirmation_config_proto_rawDescData
}
-var file_transport_internet_tlsmirror_mirrorenrollment_roundtripperenrollmentconfirmation_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_transport_internet_tlsmirror_mirrorenrollment_roundtripperenrollmentconfirmation_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_transport_internet_tlsmirror_mirrorenrollment_roundtripperenrollmentconfirmation_config_proto_goTypes = []any{
- (*ClientConfig)(nil), // 0: v2ray.core.transport.internet.tlsmirror.mirrorenrollment.roundtripperenrollmentconfirmation.ClientConfig
- (*ServerConfig)(nil), // 1: v2ray.core.transport.internet.tlsmirror.mirrorenrollment.roundtripperenrollmentconfirmation.ServerConfig
- (*anypb.Any)(nil), // 2: google.protobuf.Any
+ (*ClientConfig)(nil), // 0: v2ray.core.transport.internet.tlsmirror.mirrorenrollment.roundtripperenrollmentconfirmation.ClientConfig
+ (*ServerInverseRoleConfig)(nil), // 1: v2ray.core.transport.internet.tlsmirror.mirrorenrollment.roundtripperenrollmentconfirmation.ServerInverseRoleConfig
+ (*ServerConfig)(nil), // 2: v2ray.core.transport.internet.tlsmirror.mirrorenrollment.roundtripperenrollmentconfirmation.ServerConfig
+ (*anypb.Any)(nil), // 3: google.protobuf.Any
}
var file_transport_internet_tlsmirror_mirrorenrollment_roundtripperenrollmentconfirmation_config_proto_depIdxs = []int32{
- 2, // 0: v2ray.core.transport.internet.tlsmirror.mirrorenrollment.roundtripperenrollmentconfirmation.ClientConfig.round_tripper_client:type_name -> google.protobuf.Any
- 2, // 1: v2ray.core.transport.internet.tlsmirror.mirrorenrollment.roundtripperenrollmentconfirmation.ClientConfig.security_config:type_name -> google.protobuf.Any
- 2, // 2: v2ray.core.transport.internet.tlsmirror.mirrorenrollment.roundtripperenrollmentconfirmation.ServerConfig.round_tripper_server:type_name -> google.protobuf.Any
- 3, // [3:3] is the sub-list for method output_type
- 3, // [3:3] is the sub-list for method input_type
- 3, // [3:3] is the sub-list for extension type_name
- 3, // [3:3] is the sub-list for extension extendee
- 0, // [0:3] is the sub-list for field type_name
+ 3, // 0: v2ray.core.transport.internet.tlsmirror.mirrorenrollment.roundtripperenrollmentconfirmation.ClientConfig.round_tripper_client:type_name -> google.protobuf.Any
+ 3, // 1: v2ray.core.transport.internet.tlsmirror.mirrorenrollment.roundtripperenrollmentconfirmation.ClientConfig.security_config:type_name -> google.protobuf.Any
+ 3, // 2: v2ray.core.transport.internet.tlsmirror.mirrorenrollment.roundtripperenrollmentconfirmation.ServerInverseRoleConfig.round_tripper_client:type_name -> google.protobuf.Any
+ 3, // 3: v2ray.core.transport.internet.tlsmirror.mirrorenrollment.roundtripperenrollmentconfirmation.ServerInverseRoleConfig.security_config:type_name -> google.protobuf.Any
+ 3, // 4: v2ray.core.transport.internet.tlsmirror.mirrorenrollment.roundtripperenrollmentconfirmation.ServerConfig.round_tripper_server:type_name -> google.protobuf.Any
+ 5, // [5:5] is the sub-list for method output_type
+ 5, // [5:5] is the sub-list for method input_type
+ 5, // [5:5] is the sub-list for extension type_name
+ 5, // [5:5] is the sub-list for extension extendee
+ 0, // [0:5] is the sub-list for field type_name
}
func init() {
@@ -203,7 +288,7 @@ func file_transport_internet_tlsmirror_mirrorenrollment_roundtripperenrollmentco
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_tlsmirror_mirrorenrollment_roundtripperenrollmentconfirmation_config_proto_rawDesc), len(file_transport_internet_tlsmirror_mirrorenrollment_roundtripperenrollmentconfirmation_config_proto_rawDesc)),
NumEnums: 0,
- NumMessages: 2,
+ NumMessages: 3,
NumExtensions: 0,
NumServices: 0,
},
diff --git a/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/config.proto b/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/config.proto
index 3a4cf698ea..bc52108bae 100644
--- a/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/config.proto
+++ b/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/config.proto
@@ -17,6 +17,14 @@ message ClientConfig {
bytes server_identity = 5;
}
+message ServerInverseRoleConfig {
+ google.protobuf.Any round_tripper_client = 1;
+ google.protobuf.Any security_config = 2;
+ string dest = 3;
+ string outbound_tag = 4;
+ bytes server_identity = 5;
+}
+
message ServerConfig {
google.protobuf.Any round_tripper_server = 2;
string listen = 3;
diff --git a/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/serverinverserole.go b/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/serverinverserole.go
new file mode 100644
index 0000000000..9bef9d4663
--- /dev/null
+++ b/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/serverinverserole.go
@@ -0,0 +1,171 @@
+package roundtripperenrollmentconfirmation
+
+import (
+ "context"
+ csrand "crypto/rand"
+ "net"
+
+ "google.golang.org/protobuf/proto"
+
+ "github.com/v2fly/v2ray-core/v5/common"
+ "github.com/v2fly/v2ray-core/v5/common/environment"
+ "github.com/v2fly/v2ray-core/v5/common/environment/envctx"
+ v2net "github.com/v2fly/v2ray-core/v5/common/net"
+ "github.com/v2fly/v2ray-core/v5/common/serial"
+ "github.com/v2fly/v2ray-core/v5/transport/internet/request"
+ "github.com/v2fly/v2ray-core/v5/transport/internet/security"
+ "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror"
+)
+
+func NewServerInverseRole(ctx context.Context, config *ServerInverseRoleConfig) (*ServerInverseRole, error) {
+ if ctx == nil {
+ return nil, newError("context cannot be nil")
+ }
+
+ if config == nil {
+ return nil, newError("config cannot be nil")
+ }
+
+ rttClientConfig, err := serial.GetInstanceOf(config.RoundTripperClient)
+ if err != nil {
+ return nil, newError("failed to get instance of RoundTripperClient").Base(err)
+ }
+
+ rttClientI, err := common.CreateObject(ctx, rttClientConfig)
+ if err != nil {
+ return nil, newError("failed to create RoundTripperClient").Base(err)
+ }
+
+ rttClient, ok := rttClientI.(request.RoundTripperClient)
+ if !ok {
+ return nil, newError("RoundTripperClient is not a valid request.RoundTripperClient")
+ }
+
+ clientTemporaryIdentifier := make([]byte, 16)
+ if _, err := csrand.Read(clientTemporaryIdentifier); err != nil {
+ return nil, newError("failed to generate client temporary identifier").Base(err)
+ }
+
+ c := &ServerInverseRole{
+ ctx: ctx,
+ config: config,
+ rttClient: rttClient,
+ clientTemporaryIdentifier: clientTemporaryIdentifier,
+ }
+
+ rttClient.OnTransportClientAssemblyReady(c)
+
+ go c.worker(ctx)
+
+ return c, nil
+}
+
+type ServerInverseRole struct {
+ config *ServerInverseRoleConfig
+ rttClient request.RoundTripperClient
+
+ clientTemporaryIdentifier []byte
+
+ ctx context.Context
+
+ defaultOutboundTag string
+
+ enrollmentProcessor tlsmirror.ConnectionEnrollmentConfirmationProcessor
+}
+
+func (c *ServerInverseRole) worker(ctx context.Context) {
+ for ctx.Err() == nil {
+ err := c.pollRemoteForEnrollment(ctx)
+ if err != nil {
+ newError("error polling remote for enrollment").Base(err).AtWarning().WriteToLog()
+ }
+ }
+ newError("inverse role server quitted").AtWarning().WriteToLog()
+}
+
+func (c *ServerInverseRole) Dial(ctx context.Context) (net.Conn, error) {
+ transportEnvironment := envctx.EnvironmentFromContext(ctx).(environment.TransportEnvironment)
+ dialer := transportEnvironment.OutboundDialer()
+ if dialer == nil {
+ return nil, newError("no outbound dialer available in transport environment")
+ }
+ dest, err := v2net.ParseDestination(c.config.Dest)
+ if err != nil {
+ return nil, newError("failed to parse destination address").Base(err).AtError()
+ }
+ dest.Network = v2net.Network_TCP
+ conn, err := dialer(ctx, dest, c.config.OutboundTag)
+ if err != nil {
+ return nil, newError("failed to dial to destination").Base(err).AtError()
+ }
+ if c.config.SecurityConfig != nil {
+ securityEngine, err := common.CreateObject(ctx, c.config.SecurityConfig)
+ if err != nil {
+ return nil, newError("unable to create security engine from security settings").Base(err)
+ }
+ securityEngineTyped, ok := securityEngine.(security.Engine)
+ if !ok {
+ return nil, newError("type assertion error when create security engine from security settings")
+ }
+ conn, err = securityEngineTyped.Client(conn, security.OptionWithDestination{Dest: dest})
+ if err != nil {
+ return nil, newError("unable to create security protocol client from security engine").Base(err)
+ }
+ }
+ return conn, nil
+}
+
+func (c *ServerInverseRole) Tripper() request.Tripper {
+ return c.rttClient
+}
+
+func (c *ServerInverseRole) AutoImplDialer() request.Dialer {
+ return c
+}
+
+func (s *ServerInverseRole) OnConnectionEnrollmentConfirmationServerInstanceConfigReady(config tlsmirror.ConnectionEnrollmentConfirmationServerInstanceConfig) {
+ s.enrollmentProcessor = config.EnrollmentProcessor
+}
+
+func (s *ServerInverseRole) pollRemoteForEnrollment(ctx context.Context) error {
+ pollAs := s.config.ServerIdentity
+ req := request.Request{
+ ConnectionTag: pollAs,
+ }
+ resp, err := s.rttClient.RoundTrip(ctx, req)
+ if err != nil {
+ return newError("failed to poll remote for enrollment").Base(err)
+ }
+ if resp.Data == nil {
+ return newError("no enrollment confirmation response received from remote")
+ }
+ enrollmentReq := &tlsmirror.EnrollmentConfirmationReq{}
+ err = proto.Unmarshal(resp.Data, enrollmentReq)
+ if err != nil {
+ return newError("failed to unmarshal enrollment confirmation request").Base(err).AtError()
+ }
+ enrollmentResp, err := s.enrollmentProcessor.VerifyConnectionEnrollment(enrollmentReq)
+ if err != nil {
+ return newError("failed to process enrollment confirmation request").Base(err).AtError()
+ }
+ respData, err := proto.Marshal(enrollmentResp)
+ if err != nil {
+ return newError("failed to marshal enrollment confirmation response").Base(err).AtError()
+ }
+ respAs := append(s.config.ServerIdentity, enrollmentReq.ReplyAddressTag...) //nolint:gocritic
+ _, err = s.rttClient.RoundTrip(ctx, request.Request{
+ Data: respData,
+ ConnectionTag: respAs,
+ })
+ if err != nil {
+ return newError("failed to send enrollment confirmation response back to remote").Base(err)
+ }
+ newError("successfully processed enrollment confirmation request").AtDebug().WriteToLog()
+ return nil
+}
+
+func init() {
+ common.Must(common.RegisterConfig((*ServerInverseRoleConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
+ return NewServerInverseRole(ctx, config.(*ServerInverseRoleConfig))
+ }))
+}
diff --git a/v2ray-core/transport/internet/tlsmirror/server/client.go b/v2ray-core/transport/internet/tlsmirror/server/client.go
index aa289d126a..4bf54339bb 100644
--- a/v2ray-core/transport/internet/tlsmirror/server/client.go
+++ b/v2ray-core/transport/internet/tlsmirror/server/client.go
@@ -8,8 +8,6 @@ import (
"github.com/golang/protobuf/proto"
- "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/mirrorcommon"
-
core "github.com/v2fly/v2ray-core/v5"
"github.com/v2fly/v2ray-core/v5/common"
"github.com/v2fly/v2ray-core/v5/common/environment"
@@ -20,6 +18,7 @@ import (
"github.com/v2fly/v2ray-core/v5/transport/internet"
"github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror"
"github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/mirrorbase"
+ "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/mirrorcommon"
"github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/mirrorenrollment"
"github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/tlstrafficgen"
)
@@ -279,9 +278,15 @@ func (d *persistentMirrorTLSDialer) Dial(ctx context.Context,
if err != nil {
return nil, newError("failed to request new connection").Base(err)
}
+ timer := time.NewTimer(10 * time.Second)
+ defer timer.Stop()
select { // nolint: staticcheck
case conn := <-d.incomingConnections:
recvConn = conn
+ case <-timer.C:
+ return nil, newError("timeout waiting for incoming connection")
+ case <-ctx.Done():
+ return nil, newError("context done while waiting for incoming connection")
}
}
diff --git a/v2ray-rules-dat/.github/workflows/run.yml b/v2ray-rules-dat/.github/workflows/run.yml
index 4687e568a7..a40d0eeb43 100644
--- a/v2ray-rules-dat/.github/workflows/run.yml
+++ b/v2ray-rules-dat/.github/workflows/run.yml
@@ -31,24 +31,24 @@ jobs:
shell: bash
- name: Checkout the "hidden" branch of this repo
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
ref: hidden
- name: Checkout Loyalsoldier/domain-list-custom
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
repository: Loyalsoldier/domain-list-custom
path: custom
- name: Checkout v2fly/domain-list-community
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
repository: v2fly/domain-list-community
path: community
- name: Checkout cokebar/gfwlist2dnsmasq
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
repository: cokebar/gfwlist2dnsmasq
path: gfwlist2dnsmasq
diff --git a/v2rayn/.github/workflows/build-linux.yml b/v2rayn/.github/workflows/build-linux.yml
index 8fe09d48ad..1cf32d891e 100644
--- a/v2rayn/.github/workflows/build-linux.yml
+++ b/v2rayn/.github/workflows/build-linux.yml
@@ -31,7 +31,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v5.0.1
+ uses: actions/checkout@v6.0.0
with:
submodules: 'recursive'
fetch-depth: '0'
@@ -110,7 +110,7 @@ jobs:
dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which
- name: Checkout repo (for scripts)
- uses: actions/checkout@v5.0.1
+ uses: actions/checkout@v6.0.0
with:
submodules: 'recursive'
fetch-depth: '0'
diff --git a/v2rayn/.github/workflows/build-osx.yml b/v2rayn/.github/workflows/build-osx.yml
index b8f3e20241..61f974c42e 100644
--- a/v2rayn/.github/workflows/build-osx.yml
+++ b/v2rayn/.github/workflows/build-osx.yml
@@ -26,7 +26,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v5.0.1
+ uses: actions/checkout@v6.0.0
with:
submodules: 'recursive'
fetch-depth: '0'
diff --git a/v2rayn/.github/workflows/build-windows-desktop.yml b/v2rayn/.github/workflows/build-windows-desktop.yml
index 73f679fe27..aa7d0c116c 100644
--- a/v2rayn/.github/workflows/build-windows-desktop.yml
+++ b/v2rayn/.github/workflows/build-windows-desktop.yml
@@ -26,7 +26,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v5.0.1
+ uses: actions/checkout@v6.0.0
with:
submodules: 'recursive'
fetch-depth: '0'
diff --git a/v2rayn/.github/workflows/build-windows.yml b/v2rayn/.github/workflows/build-windows.yml
index 0ffe971aea..e779e12cdb 100644
--- a/v2rayn/.github/workflows/build-windows.yml
+++ b/v2rayn/.github/workflows/build-windows.yml
@@ -27,7 +27,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v5.0.1
+ uses: actions/checkout@v6.0.0
- name: Setup
uses: actions/setup-dotnet@v5.0.0
diff --git a/v2rayn/v2rayN/ServiceLib/Handler/Fmt/ShadowsocksFmt.cs b/v2rayn/v2rayN/ServiceLib/Handler/Fmt/ShadowsocksFmt.cs
index 04f52232be..a17a8bd8b1 100644
--- a/v2rayn/v2rayN/ServiceLib/Handler/Fmt/ShadowsocksFmt.cs
+++ b/v2rayn/v2rayN/ServiceLib/Handler/Fmt/ShadowsocksFmt.cs
@@ -73,13 +73,11 @@ public class ShadowsocksFmt : BaseFmt
const string beginMarker = "-----BEGIN CERTIFICATE-----\n";
const string endMarker = "\n-----END CERTIFICATE-----";
- var base64Start = beginMarker.Length;
- var endIndex = cert.IndexOf(endMarker, base64Start, StringComparison.Ordinal);
- var base64Content = cert.Substring(base64Start, endIndex - base64Start);
+ var base64Content = cert.Replace(beginMarker, "").Replace(endMarker, "").Trim();
// https://github.com/shadowsocks/v2ray-plugin/blob/e9af1cdd2549d528deb20a4ab8d61c5fbe51f306/args.go#L172
// Equal signs and commas [and backslashes] must be escaped with a backslash.
- base64Content = base64Content.Replace("\\", "\\\\").Replace("=", "\\=").Replace(",", "\\,");
+ base64Content = base64Content.Replace("=", "\\=");
pluginArgs += $"certRaw={base64Content};";
}
@@ -251,7 +249,7 @@ public class ShadowsocksFmt : BaseFmt
{
var certBase64 = certRaw.Replace("certRaw=", "");
- certBase64 = certBase64.Replace("\\=", "=").Replace("\\,", ",").Replace("\\\\", "\\");
+ certBase64 = certBase64.Replace("\\=", "=");
const string beginMarker = "-----BEGIN CERTIFICATE-----\n";
const string endMarker = "\n-----END CERTIFICATE-----";
diff --git a/v2rayn/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs b/v2rayn/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs
index 75e3e00ebb..7b665816a2 100644
--- a/v2rayn/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs
+++ b/v2rayn/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs
@@ -10,6 +10,13 @@ public class ActionPrecheckManager(Config config)
private readonly Config _config = config;
+ // sing-box supported transports for different protocol types
+ private static readonly HashSet SingboxUnsupportedTransports = [nameof(ETransport.kcp), nameof(ETransport.xhttp)];
+ private static readonly HashSet SingboxTransportSupportedProtocols =
+ [EConfigType.VMess, EConfigType.VLESS, EConfigType.Trojan, EConfigType.Shadowsocks];
+ private static readonly HashSet SingboxShadowsocksAllowedTransports =
+ [nameof(ETransport.tcp), nameof(ETransport.ws), nameof(ETransport.quic)];
+
public async Task> Check(string? indexId)
{
if (indexId.IsNullOrEmpty())
@@ -174,26 +181,16 @@ public class ActionPrecheckManager(Config config)
return errors;
}
- var net = item.GetNetwork() ?? item.Network;
+ var net = item.GetNetwork();
if (coreType == ECoreType.sing_box)
{
- // sing-box does not support xhttp / kcp
- // sing-box does not support transports like ws/http/httpupgrade/etc. when the node is not vmess/trojan/vless
- if (net is nameof(ETransport.kcp) or nameof(ETransport.xhttp))
+ var transportError = ValidateSingboxTransport(item.ConfigType, net);
+ if (transportError != null)
{
- errors.Add(string.Format(ResUI.CoreNotSupportNetwork, nameof(ECoreType.sing_box), net));
+ errors.Add(transportError);
return errors;
}
-
- if (item.ConfigType is not (EConfigType.VMess or EConfigType.VLESS or EConfigType.Trojan))
- {
- if (net is nameof(ETransport.ws) or nameof(ETransport.http) or nameof(ETransport.h2) or nameof(ETransport.quic) or nameof(ETransport.httpupgrade))
- {
- errors.Add(string.Format(ResUI.CoreNotSupportProtocolTransport, nameof(ECoreType.sing_box), item.ConfigType.ToString(), net));
- return errors;
- }
- }
}
else if (coreType is ECoreType.Xray)
{
@@ -209,6 +206,31 @@ public class ActionPrecheckManager(Config config)
return errors;
}
+ private static string? ValidateSingboxTransport(EConfigType configType, string net)
+ {
+ // sing-box does not support xhttp / kcp transports
+ if (SingboxUnsupportedTransports.Contains(net))
+ {
+ return string.Format(ResUI.CoreNotSupportNetwork, nameof(ECoreType.sing_box), net);
+ }
+
+ // sing-box does not support non-tcp transports for protocols other than vmess/trojan/vless/shadowsocks
+ if (!SingboxTransportSupportedProtocols.Contains(configType) && net != nameof(ETransport.tcp))
+ {
+ return string.Format(ResUI.CoreNotSupportProtocolTransport,
+ nameof(ECoreType.sing_box), configType.ToString(), net);
+ }
+
+ // sing-box shadowsocks only supports tcp/ws/quic transports
+ if (configType == EConfigType.Shadowsocks && !SingboxShadowsocksAllowedTransports.Contains(net))
+ {
+ return string.Format(ResUI.CoreNotSupportProtocolTransport,
+ nameof(ECoreType.sing_box), configType.ToString(), net);
+ }
+
+ return null;
+ }
+
private async Task> ValidateRelatedNodesExistAndValid(ProfileItem? item)
{
var errors = new List();
diff --git a/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs b/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs
index 1bda5981ff..5188d1774c 100644
--- a/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs
+++ b/v2rayn/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs
@@ -62,13 +62,11 @@ public partial class CoreConfigSingboxService
const string beginMarker = "-----BEGIN CERTIFICATE-----\n";
const string endMarker = "\n-----END CERTIFICATE-----";
- var base64Start = beginMarker.Length;
- var endIndex = cert.IndexOf(endMarker, base64Start, StringComparison.Ordinal);
- var base64Content = cert.Substring(base64Start, endIndex - base64Start);
+ var base64Content = cert.Replace(beginMarker, "").Replace(endMarker, "").Trim();
// https://github.com/shadowsocks/v2ray-plugin/blob/e9af1cdd2549d528deb20a4ab8d61c5fbe51f306/args.go#L172
// Equal signs and commas [and backslashes] must be escaped with a backslash.
- base64Content = base64Content.Replace("\\", "\\\\").Replace("=", "\\=").Replace(",", "\\,");
+ base64Content = base64Content.Replace("=", "\\=");
pluginArgs += $"certRaw={base64Content};";
}
diff --git a/v2rayn/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml b/v2rayn/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml
index 2fa4e9eab0..52cf5c4fb3 100644
--- a/v2rayn/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml
+++ b/v2rayn/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml
@@ -90,13 +90,13 @@
-
-
+
+
diff --git a/v2rayn/v2rayN/v2rayN/Views/BackupAndRestoreView.xaml b/v2rayn/v2rayN/v2rayN/Views/BackupAndRestoreView.xaml
index a803d52406..3b50cfe44b 100644
--- a/v2rayn/v2rayN/v2rayN/Views/BackupAndRestoreView.xaml
+++ b/v2rayn/v2rayN/v2rayN/Views/BackupAndRestoreView.xaml
@@ -1,4 +1,4 @@
-
-
-
-
\ No newline at end of file
+
diff --git a/v2rayn/v2rayN/v2rayN/Views/CheckUpdateView.xaml b/v2rayn/v2rayN/v2rayN/Views/CheckUpdateView.xaml
index b619f72b37..7a8a84f769 100644
--- a/v2rayn/v2rayN/v2rayN/Views/CheckUpdateView.xaml
+++ b/v2rayn/v2rayN/v2rayN/Views/CheckUpdateView.xaml
@@ -1,4 +1,4 @@
-
-
-
@@ -99,4 +90,4 @@
-
\ No newline at end of file
+
diff --git a/v2rayn/v2rayN/v2rayN/Views/MainWindow.xaml b/v2rayn/v2rayN/v2rayN/Views/MainWindow.xaml
index 5d6fc7f566..25de8f7fd4 100644
--- a/v2rayn/v2rayN/v2rayN/Views/MainWindow.xaml
+++ b/v2rayn/v2rayN/v2rayN/Views/MainWindow.xaml
@@ -32,6 +32,7 @@
@@ -231,23 +232,6 @@
-
-
+
+