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 @@ - -