diff --git a/.github/update.log b/.github/update.log index 769adc2fa3..abcdcfd4b1 100644 --- a/.github/update.log +++ b/.github/update.log @@ -1030,3 +1030,4 @@ Update On Fri Jun 13 20:38:23 CEST 2025 Update On Sat Jun 14 20:35:08 CEST 2025 Update On Sun Jun 15 20:35:35 CEST 2025 Update On Mon Jun 16 20:38:07 CEST 2025 +Update On Tue Jun 17 20:38:11 CEST 2025 diff --git a/clash-nyanpasu/frontend/nyanpasu/package.json b/clash-nyanpasu/frontend/nyanpasu/package.json index 9b04b4acf2..e52ffc0439 100644 --- a/clash-nyanpasu/frontend/nyanpasu/package.json +++ b/clash-nyanpasu/frontend/nyanpasu/package.json @@ -43,12 +43,12 @@ "react-error-boundary": "6.0.0", "react-fast-marquee": "1.6.5", "react-hook-form-mui": "7.6.1", - "react-i18next": "15.5.2", + "react-i18next": "15.5.3", "react-markdown": "10.1.0", "react-split-grid": "1.0.4", "react-use": "17.6.0", "swr": "2.3.3", - "virtua": "0.41.3", + "virtua": "0.41.5", "vite-bundle-visualizer": "1.2.1" }, "devDependencies": { diff --git a/clash-nyanpasu/frontend/ui/package.json b/clash-nyanpasu/frontend/ui/package.json index e9d9cf03eb..ee4fc6ef69 100644 --- a/clash-nyanpasu/frontend/ui/package.json +++ b/clash-nyanpasu/frontend/ui/package.json @@ -27,7 +27,7 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-error-boundary": "6.0.0", - "react-i18next": "15.5.2", + "react-i18next": "15.5.3", "react-use": "17.6.0", "tailwindcss": "4.1.8", "vite": "6.3.5", diff --git a/clash-nyanpasu/package.json b/clash-nyanpasu/package.json index 47501b7ea3..8f52988cc1 100644 --- a/clash-nyanpasu/package.json +++ b/clash-nyanpasu/package.json @@ -88,9 +88,9 @@ "lint-staged": "16.1.0", "neostandard": "0.12.1", "npm-run-all2": "8.0.4", - "postcss": "8.5.5", + "postcss": "8.5.6", "postcss-html": "1.8.0", - "postcss-import": "16.1.0", + "postcss-import": "16.1.1", "postcss-scss": "4.0.9", "prettier": "3.5.3", "prettier-plugin-tailwindcss": "0.6.12", diff --git a/clash-nyanpasu/pnpm-lock.yaml b/clash-nyanpasu/pnpm-lock.yaml index 506014829b..3dcd9401c4 100644 --- a/clash-nyanpasu/pnpm-lock.yaml +++ b/clash-nyanpasu/pnpm-lock.yaml @@ -54,7 +54,7 @@ importers: version: 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) autoprefixer: specifier: 10.4.21 - version: 10.4.21(postcss@8.5.5) + version: 10.4.21(postcss@8.5.6) conventional-changelog-conventionalcommits: specifier: 9.0.0 version: 9.0.0 @@ -113,17 +113,17 @@ importers: specifier: 8.0.4 version: 8.0.4 postcss: - specifier: 8.5.5 - version: 8.5.5 + specifier: 8.5.6 + version: 8.5.6 postcss-html: specifier: 1.8.0 version: 1.8.0 postcss-import: - specifier: 16.1.0 - version: 16.1.0(postcss@8.5.5) + specifier: 16.1.1 + version: 16.1.1(postcss@8.5.6) postcss-scss: specifier: 4.0.9 - version: 4.0.9(postcss@8.5.5) + version: 4.0.9(postcss@8.5.6) prettier: specifier: 3.5.3 version: 3.5.3 @@ -306,8 +306,8 @@ importers: specifier: 7.6.1 version: 7.6.1(a709df8d501e7bdf4d7009cd73340429) react-i18next: - specifier: 15.5.2 - version: 15.5.2(i18next@25.2.1(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) + specifier: 15.5.3 + version: 15.5.3(i18next@25.2.1(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) react-markdown: specifier: 10.1.0 version: 10.1.0(@types/react@19.1.6)(react@19.1.0) @@ -321,8 +321,8 @@ importers: specifier: 2.3.3 version: 2.3.3(react@19.1.0) virtua: - specifier: 0.41.3 - version: 0.41.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.5) + specifier: 0.41.5 + version: 0.41.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.5) vite-bundle-visualizer: specifier: 1.2.1 version: 1.2.1(rollup@4.40.0) @@ -440,7 +440,7 @@ importers: version: 3.2.2(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)) vite-plugin-sass-dts: specifier: 1.3.31 - version: 1.3.31(postcss@8.5.5)(prettier@3.5.3)(sass-embedded@1.88.0)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)) + version: 1.3.31(postcss@8.5.6)(prettier@3.5.3)(sass-embedded@1.88.0)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)) vite-plugin-svgr: specifier: 4.3.0 version: 4.3.0(rollup@4.40.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)) @@ -502,8 +502,8 @@ importers: specifier: 6.0.0 version: 6.0.0(react@19.1.0) react-i18next: - specifier: 15.5.2 - version: 15.5.2(i18next@25.2.1(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) + specifier: 15.5.3 + version: 15.5.3(i18next@25.2.1(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) react-use: specifier: 17.6.0 version: 17.6.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -6513,8 +6513,8 @@ packages: resolution: {integrity: sha512-5mMeb1TgLWoRKxZ0Xh9RZDfwUUIqRrcxO2uXO+Ezl1N5lqpCiSU5Gk6+1kZediBfBHFtPCdopr2UZ2SgUsKcgQ==} engines: {node: ^12 || >=14} - postcss-import@16.1.0: - resolution: {integrity: sha512-7hsAZ4xGXl4MW+OKEWCnF6T5jqBw80/EE9aXg1r2yyn1RsVEU8EtKXbijEODa+rg7iih4bKf7vlvTGYR4CnPNg==} + postcss-import@16.1.1: + resolution: {integrity: sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ==} engines: {node: '>=18.0.0'} peerDependencies: postcss: ^8.0.0 @@ -6600,8 +6600,8 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.5: - resolution: {integrity: sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==} + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -6766,8 +6766,8 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-i18next@15.5.2: - resolution: {integrity: sha512-ePODyXgmZQAOYTbZXQn5rRsSBu3Gszo69jxW6aKmlSgxKAI1fOhDwSu6bT4EKHciWPKQ7v7lPrjeiadR6Gi+1A==} + react-i18next@15.5.3: + resolution: {integrity: sha512-ypYmOKOnjqPEJZO4m1BI0kS8kWqkBNsKYyhVUfij0gvjy9xJNoG/VcGkxq5dRlVwzmrmY1BQMAmpbbUBLwC4Kw==} peerDependencies: i18next: '>= 23.2.3' react: '>= 16.8.0' @@ -7955,8 +7955,8 @@ packages: vfile@6.0.1: resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==} - virtua@0.41.3: - resolution: {integrity: sha512-X0eG4hIsJFrmgiq5kiyxdBqu+83mn2ArO9L16flRg1cN0BrafOmRVYmYijgClKElTlhqLJ57cdGby0X6az2LGw==} + virtua@0.41.5: + resolution: {integrity: sha512-x1vsA9qIQNBFcCs1rzCjyYdMvDu/kT6o6zwwQnyqFOFdOyIzqyzU3WfR/hJC8WxUZXSCo2LkuoqapL8VDDMQPg==} peerDependencies: react: '>=16.14.0' react-dom: '>=16.14.0' @@ -10593,7 +10593,7 @@ snapshots: '@alloc/quick-lru': 5.2.0 '@tailwindcss/node': 4.1.8 '@tailwindcss/oxide': 4.1.8 - postcss: 8.5.5 + postcss: 8.5.6 tailwindcss: 4.1.8 '@tanstack/history@1.120.17': {} @@ -11048,11 +11048,11 @@ snapshots: '@types/postcss-modules-local-by-default@4.0.2': dependencies: - postcss: 8.5.5 + postcss: 8.5.6 '@types/postcss-modules-scope@3.0.4': dependencies: - postcss: 8.5.5 + postcss: 8.5.6 '@types/prop-types@15.7.14': {} @@ -11552,14 +11552,14 @@ snapshots: async@3.2.6: {} - autoprefixer@10.4.21(postcss@8.5.5): + autoprefixer@10.4.21(postcss@8.5.6): dependencies: browserslist: 4.24.4 caniuse-lite: 1.0.30001702 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 - postcss: 8.5.5 + postcss: 8.5.6 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -13478,9 +13478,9 @@ snapshots: dependencies: safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.5.5): + icss-utils@5.1.0(postcss@8.5.6): dependencies: - postcss: 8.5.5 + postcss: 8.5.6 ieee754@1.2.1: {} @@ -14870,59 +14870,59 @@ snapshots: dependencies: htmlparser2: 8.0.2 js-tokens: 9.0.1 - postcss: 8.5.5 - postcss-safe-parser: 6.0.0(postcss@8.5.5) + postcss: 8.5.6 + postcss-safe-parser: 6.0.0(postcss@8.5.6) - postcss-import@16.1.0(postcss@8.5.5): + postcss-import@16.1.1(postcss@8.5.6): dependencies: - postcss: 8.5.5 + postcss: 8.5.6 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.8 - postcss-js@4.0.1(postcss@8.5.5): + postcss-js@4.0.1(postcss@8.5.6): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.5 + postcss: 8.5.6 - postcss-load-config@3.1.4(postcss@8.5.5): + postcss-load-config@3.1.4(postcss@8.5.6): dependencies: lilconfig: 2.1.0 yaml: 1.10.2 optionalDependencies: - postcss: 8.5.5 + postcss: 8.5.6 postcss-media-query-parser@0.2.3: {} - postcss-modules-extract-imports@3.1.0(postcss@8.5.5): + postcss-modules-extract-imports@3.1.0(postcss@8.5.6): dependencies: - postcss: 8.5.5 + postcss: 8.5.6 - postcss-modules-local-by-default@4.0.5(postcss@8.5.5): + postcss-modules-local-by-default@4.0.5(postcss@8.5.6): dependencies: - icss-utils: 5.1.0(postcss@8.5.5) - postcss: 8.5.5 + icss-utils: 5.1.0(postcss@8.5.6) + postcss: 8.5.6 postcss-selector-parser: 6.1.2 postcss-value-parser: 4.2.0 - postcss-modules-scope@3.2.0(postcss@8.5.5): + postcss-modules-scope@3.2.0(postcss@8.5.6): dependencies: - postcss: 8.5.5 + postcss: 8.5.6 postcss-selector-parser: 6.1.2 postcss-resolve-nested-selector@0.1.6: {} - postcss-safe-parser@6.0.0(postcss@8.5.5): + postcss-safe-parser@6.0.0(postcss@8.5.6): dependencies: - postcss: 8.5.5 + postcss: 8.5.6 - postcss-safe-parser@7.0.1(postcss@8.5.5): + postcss-safe-parser@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.5.5 + postcss: 8.5.6 - postcss-scss@4.0.9(postcss@8.5.5): + postcss-scss@4.0.9(postcss@8.5.6): dependencies: - postcss: 8.5.5 + postcss: 8.5.6 postcss-selector-parser@6.1.2: dependencies: @@ -14934,17 +14934,17 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-sorting@8.0.2(postcss@8.5.5): + postcss-sorting@8.0.2(postcss@8.5.6): dependencies: - postcss: 8.5.5 + postcss: 8.5.6 - postcss-sorting@9.1.0(postcss@8.5.5): + postcss-sorting@9.1.0(postcss@8.5.6): dependencies: - postcss: 8.5.5 + postcss: 8.5.6 postcss-value-parser@4.2.0: {} - postcss@8.5.5: + postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -15058,9 +15058,9 @@ snapshots: dependencies: react: 19.1.0 - react-i18next@15.5.2(i18next@25.2.1(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3): + react-i18next@15.5.3(i18next@25.2.1(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3): dependencies: - '@babel/runtime': 7.27.1 + '@babel/runtime': 7.27.6 html-parse-stringify: 3.0.1 i18next: 25.2.1(typescript@5.8.3) react: 19.1.0 @@ -15826,14 +15826,14 @@ snapshots: stylelint-order@6.0.4(stylelint@16.20.0(typescript@5.8.3)): dependencies: - postcss: 8.5.5 - postcss-sorting: 8.0.2(postcss@8.5.5) + postcss: 8.5.6 + postcss-sorting: 8.0.2(postcss@8.5.6) stylelint: 16.20.0(typescript@5.8.3) stylelint-order@7.0.0(stylelint@16.20.0(typescript@5.8.3)): dependencies: - postcss: 8.5.5 - postcss-sorting: 9.1.0(postcss@8.5.5) + postcss: 8.5.6 + postcss-sorting: 9.1.0(postcss@8.5.6) stylelint: 16.20.0(typescript@5.8.3) stylelint-scss@6.12.0(stylelint@16.20.0(typescript@5.8.3)): @@ -15877,9 +15877,9 @@ snapshots: micromatch: 4.0.8 normalize-path: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.5 + postcss: 8.5.6 postcss-resolve-nested-selector: 0.1.6 - postcss-safe-parser: 7.0.1(postcss@8.5.5) + postcss-safe-parser: 7.0.1(postcss@8.5.6) postcss-selector-parser: 7.1.0 postcss-value-parser: 4.2.0 resolve-from: 5.0.0 @@ -16183,14 +16183,14 @@ snapshots: '@types/postcss-modules-local-by-default': 4.0.2 '@types/postcss-modules-scope': 3.0.4 dotenv: 16.4.5 - icss-utils: 5.1.0(postcss@8.5.5) + icss-utils: 5.1.0(postcss@8.5.6) less: 4.2.0 lodash.camelcase: 4.3.0 - postcss: 8.5.5 - postcss-load-config: 3.1.4(postcss@8.5.5) - postcss-modules-extract-imports: 3.1.0(postcss@8.5.5) - postcss-modules-local-by-default: 4.0.5(postcss@8.5.5) - postcss-modules-scope: 3.2.0(postcss@8.5.5) + postcss: 8.5.6 + postcss-load-config: 3.1.4(postcss@8.5.6) + postcss-modules-extract-imports: 3.1.0(postcss@8.5.6) + postcss-modules-local-by-default: 4.0.5(postcss@8.5.6) + postcss-modules-scope: 3.2.0(postcss@8.5.6) reserved-words: 0.1.2 sass: 1.83.0 source-map-js: 1.2.1 @@ -16408,7 +16408,7 @@ snapshots: unist-util-stringify-position: 4.0.0 vfile-message: 4.0.2 - virtua@0.41.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.5): + virtua@0.41.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.5): optionalDependencies: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) @@ -16459,10 +16459,10 @@ snapshots: pathe: 0.2.0 vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0) - vite-plugin-sass-dts@1.3.31(postcss@8.5.5)(prettier@3.5.3)(sass-embedded@1.88.0)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)): + vite-plugin-sass-dts@1.3.31(postcss@8.5.6)(prettier@3.5.3)(sass-embedded@1.88.0)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0)): dependencies: - postcss: 8.5.5 - postcss-js: 4.0.1(postcss@8.5.5) + postcss: 8.5.6 + postcss-js: 4.0.1(postcss@8.5.6) prettier: 3.5.3 sass-embedded: 1.88.0 vite: 6.3.5(@types/node@22.15.30)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.88.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.8.0) @@ -16494,7 +16494,7 @@ snapshots: esbuild: 0.25.0 fdir: 6.4.4(picomatch@4.0.2) picomatch: 4.0.2 - postcss: 8.5.5 + postcss: 8.5.6 rollup: 4.40.0 tinyglobby: 0.2.13 optionalDependencies: diff --git a/clash-verge-rev/.github/workflows/alpha.yml b/clash-verge-rev/.github/workflows/alpha.yml index 0c5cfa3bd3..6c21bc2289 100644 --- a/clash-verge-rev/.github/workflows/alpha.yml +++ b/clash-verge-rev/.github/workflows/alpha.yml @@ -286,8 +286,7 @@ jobs: uses: Swatinem/rust-cache@v2 with: workspaces: src-tauri - cache-all-crates: true - cache-on-failure: true + save-if: false - name: Install dependencies (ubuntu only) if: matrix.os == 'ubuntu-22.04' @@ -308,7 +307,7 @@ jobs: - name: Pnpm install and check run: | pnpm i - pnpm prepare ${{ matrix.target }} + pnpm run prebuild ${{ matrix.target }} # - name: Release ${{ env.TAG_CHANNEL }} Version # run: pnpm release-version ${{ env.TAG_NAME }} @@ -363,7 +362,7 @@ jobs: uses: Swatinem/rust-cache@v2 with: workspaces: src-tauri - cache-all-crates: true + save-if: false - name: Install Node uses: actions/setup-node@v4 @@ -378,7 +377,7 @@ jobs: - name: Pnpm install and check run: | pnpm i - pnpm prepare ${{ matrix.target }} + pnpm run prebuild ${{ matrix.target }} # - name: Release ${{ env.TAG_CHANNEL }} Version # run: pnpm release-version ${{ env.TAG_NAME }} @@ -491,8 +490,7 @@ jobs: uses: Swatinem/rust-cache@v2 with: workspaces: src-tauri - cache-all-crates: true - cache-on-failure: true + save-if: false - name: Install Node uses: actions/setup-node@v4 @@ -507,7 +505,7 @@ jobs: - name: Pnpm install and check run: | pnpm i - pnpm prepare ${{ matrix.target }} + pnpm run prebuild ${{ matrix.target }} # - name: Release ${{ env.TAG_CHANNEL }} Version # run: pnpm release-version ${{ env.TAG_NAME }} diff --git a/clash-verge-rev/.github/workflows/autobuild.yml b/clash-verge-rev/.github/workflows/autobuild.yml index 32c3aa63a9..6ce876ce76 100644 --- a/clash-verge-rev/.github/workflows/autobuild.yml +++ b/clash-verge-rev/.github/workflows/autobuild.yml @@ -183,7 +183,7 @@ jobs: with: workspaces: src-tauri cache-all-crates: true - cache-on-failure: true + save-if: ${{ github.ref == 'refs/heads/dev' }} - name: Install dependencies (ubuntu only) if: matrix.os == 'ubuntu-22.04' @@ -195,6 +195,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: "22" + cache: "pnpm" - uses: pnpm/action-setup@v4 name: Install pnpm @@ -204,7 +205,7 @@ jobs: - name: Pnpm install and check run: | pnpm i - pnpm prepare ${{ matrix.target }} + pnpm run prebuild ${{ matrix.target }} - name: Release ${{ env.TAG_CHANNEL }} Version run: pnpm release-version ${{ env.TAG_NAME }} @@ -260,11 +261,13 @@ jobs: with: workspaces: src-tauri cache-all-crates: true + save-if: ${{ github.ref == 'refs/heads/dev' }} - name: Install Node uses: actions/setup-node@v4 with: node-version: "22" + cache: "pnpm" - name: Install pnpm uses: pnpm/action-setup@v4 @@ -274,7 +277,7 @@ jobs: - name: Pnpm install and check run: | pnpm i - pnpm prepare ${{ matrix.target }} + pnpm run prebuild ${{ matrix.target }} - name: Release ${{ env.TAG_CHANNEL }} Version run: pnpm release-version ${{ env.TAG_NAME }} @@ -388,12 +391,13 @@ jobs: with: workspaces: src-tauri cache-all-crates: true - cache-on-failure: true + save-if: ${{ github.ref == 'refs/heads/dev' }} - name: Install Node uses: actions/setup-node@v4 with: node-version: "22" + cache: "pnpm" - uses: pnpm/action-setup@v4 name: Install pnpm @@ -403,7 +407,7 @@ jobs: - name: Pnpm install and check run: | pnpm i - pnpm prepare ${{ matrix.target }} + pnpm run prebuild ${{ matrix.target }} - name: Release ${{ env.TAG_CHANNEL }} Version run: pnpm release-version ${{ env.TAG_NAME }} diff --git a/clash-verge-rev/.github/workflows/clippy.yml b/clash-verge-rev/.github/workflows/clippy.yml index f805ce1a71..64392ab3bf 100644 --- a/clash-verge-rev/.github/workflows/clippy.yml +++ b/clash-verge-rev/.github/workflows/clippy.yml @@ -27,12 +27,11 @@ jobs: - name: Add Rust Target run: rustup target add ${{ matrix.target }} - # - name: Rust Cache - # uses: Swatinem/rust-cache@v2 - # with: - # workspaces: src-tauri - # cache-all-crates: true - # cache-on-failure: true + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + save-if: false - name: Install dependencies (ubuntu only) if: matrix.os == 'ubuntu-22.04' @@ -53,7 +52,7 @@ jobs: - name: Pnpm install and check run: | pnpm i - pnpm prepare ${{ matrix.target }} + pnpm run prebuild ${{ matrix.target }} - name: Build Web Assets run: pnpm run web:build diff --git a/clash-verge-rev/.github/workflows/cross_check.yaml b/clash-verge-rev/.github/workflows/cross_check.yaml index eaf1b315a1..e00099c410 100644 --- a/clash-verge-rev/.github/workflows/cross_check.yaml +++ b/clash-verge-rev/.github/workflows/cross_check.yaml @@ -50,14 +50,13 @@ jobs: - name: Pnpm install and check run: | pnpm i - pnpm prepare ${{ matrix.target }} + pnpm run prebuild ${{ matrix.target }} - name: Rust Cache uses: Swatinem/rust-cache@v2 with: workspaces: src-tauri - cache-all-crates: true - cache-on-failure: true + save-if: false - name: Cargo Check (deny warnings) working-directory: src-tauri diff --git a/clash-verge-rev/.github/workflows/dev.yml b/clash-verge-rev/.github/workflows/dev.yml index 0ab6dd7e64..53b771cc0f 100644 --- a/clash-verge-rev/.github/workflows/dev.yml +++ b/clash-verge-rev/.github/workflows/dev.yml @@ -42,8 +42,7 @@ jobs: uses: Swatinem/rust-cache@v2 with: workspaces: src-tauri - cache-all-crates: true - cache-on-failure: true + save-if: false - name: Install Node uses: actions/setup-node@v4 @@ -58,7 +57,7 @@ jobs: - name: Pnpm install and check run: | pnpm i - pnpm prepare ${{ matrix.target }} + pnpm run prebuild ${{ matrix.target }} - name: Tauri build uses: tauri-apps/tauri-action@v0 diff --git a/clash-verge-rev/.github/workflows/fmt.yml b/clash-verge-rev/.github/workflows/fmt.yml index 1eef2560a4..91d7e705bb 100644 --- a/clash-verge-rev/.github/workflows/fmt.yml +++ b/clash-verge-rev/.github/workflows/fmt.yml @@ -30,7 +30,6 @@ jobs: - uses: actions/setup-node@v4 with: node-version: "lts/*" - cache: "pnpm" - run: pnpm i --frozen-lockfile - run: pnpm format:check diff --git a/clash-verge-rev/.github/workflows/release.yml b/clash-verge-rev/.github/workflows/release.yml index 0dc19e606d..e101675cd5 100644 --- a/clash-verge-rev/.github/workflows/release.yml +++ b/clash-verge-rev/.github/workflows/release.yml @@ -73,8 +73,7 @@ jobs: uses: Swatinem/rust-cache@v2 with: workspaces: src-tauri - cache-all-crates: true - cache-on-failure: true + save-if: false - name: Install dependencies (ubuntu only) if: matrix.os == 'ubuntu-22.04' @@ -95,7 +94,7 @@ jobs: - name: Pnpm install and check run: | pnpm i - pnpm prepare ${{ matrix.target }} + pnpm run prebuild ${{ matrix.target }} - name: Tauri build uses: tauri-apps/tauri-action@v0 @@ -144,7 +143,7 @@ jobs: uses: Swatinem/rust-cache@v2 with: workspaces: src-tauri - cache-all-crates: true + save-if: false - name: Install Node uses: actions/setup-node@v4 @@ -159,7 +158,7 @@ jobs: - name: Pnpm install and check run: | pnpm i - pnpm prepare ${{ matrix.target }} + pnpm run prebuild ${{ matrix.target }} - name: "Setup for linux" run: |- @@ -263,8 +262,7 @@ jobs: uses: Swatinem/rust-cache@v2 with: workspaces: src-tauri - cache-all-crates: true - cache-on-failure: true + save-if: false - name: Install Node uses: actions/setup-node@v4 @@ -279,7 +277,7 @@ jobs: - name: Pnpm install and check run: | pnpm i - pnpm prepare ${{ matrix.target }} + pnpm run prebuild ${{ matrix.target }} - name: Download WebView2 Runtime run: | diff --git a/clash-verge-rev/CONTRIBUTING.md b/clash-verge-rev/CONTRIBUTING.md index 45a6c6962e..e463131388 100644 --- a/clash-verge-rev/CONTRIBUTING.md +++ b/clash-verge-rev/CONTRIBUTING.md @@ -52,9 +52,9 @@ You have two options for downloading the clash binary: - Automatically download it via the provided script: ```shell - pnpm run prepare + pnpm run prebuild # Use '--force' to force update to the latest version - # pnpm run prepare --force + # pnpm run prebuild --force ``` - Manually download it from the [Mihomo release](https://github.com/MetaCubeX/mihomo/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin). diff --git a/clash-verge-rev/UPDATELOG.md b/clash-verge-rev/UPDATELOG.md index de1ae001da..553619bf96 100644 --- a/clash-verge-rev/UPDATELOG.md +++ b/clash-verge-rev/UPDATELOG.md @@ -3,6 +3,13 @@ ### 🐞 修复问题 - 增加配置文件校验,修复从古老版本升级上来的"No such file or directory (os error 2)"错误 +- 修复扩展脚本转义错误 +- 修复 macOS Intel X86 架构构建错误导致无法运行 +- 修复 Linux 下界面边框白边问题 + +### ✨ 新增功能 + +- 新增 window-state 窗口状态管理和恢复 ## v2.3.0 diff --git a/clash-verge-rev/package.json b/clash-verge-rev/package.json index 8e6d25b282..d1ab192ed9 100644 --- a/clash-verge-rev/package.json +++ b/clash-verge-rev/package.json @@ -1,6 +1,6 @@ { "name": "clash-verge", - "version": "2.3.0", + "version": "2.3.1", "license": "GPL-3.0-only", "scripts": { "dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev", @@ -11,7 +11,7 @@ "web:dev": "vite", "web:build": "tsc --noEmit && vite build", "web:serve": "vite preview", - "prepare": "node scripts/prepare.mjs", + "prebuild": "node scripts/prebuild.mjs", "updater": "node scripts/updater.mjs", "updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs", "portable": "node scripts/portable.mjs", @@ -34,27 +34,27 @@ "@mui/icons-material": "^7.1.1", "@mui/lab": "7.0.0-beta.13", "@mui/material": "^7.1.1", - "@mui/x-data-grid": "^8.5.1", + "@mui/x-data-grid": "^8.5.2", "@tauri-apps/api": "2.5.0", - "@tauri-apps/plugin-clipboard-manager": "^2.2.2", + "@tauri-apps/plugin-clipboard-manager": "^2.2.3", "@tauri-apps/plugin-dialog": "^2.2.2", "@tauri-apps/plugin-fs": "^2.3.0", "@tauri-apps/plugin-global-shortcut": "^2.2.1", - "@tauri-apps/plugin-notification": "^2.2.2", - "@tauri-apps/plugin-process": "^2.2.1", - "@tauri-apps/plugin-shell": "2.2.1", - "@tauri-apps/plugin-updater": "2.7.1", - "@tauri-apps/plugin-window-state": "^2.2.2", + "@tauri-apps/plugin-notification": "^2.2.3", + "@tauri-apps/plugin-process": "^2.2.2", + "@tauri-apps/plugin-shell": "2.2.2", + "@tauri-apps/plugin-updater": "2.8.1", + "@tauri-apps/plugin-window-state": "^2.2.3", "@types/d3-shape": "^3.1.7", "@types/json-schema": "^7.0.15", "ahooks": "^3.8.5", - "axios": "^1.9.0", - "chart.js": "^4.4.9", + "axios": "^1.10.0", + "chart.js": "^4.5.0", "cli-color": "^2.0.4", "d3-shape": "^3.2.0", "dayjs": "1.11.13", "foxact": "^0.2.49", - "glob": "^11.0.2", + "glob": "^11.0.3", "i18next": "^25.2.1", "js-base64": "^3.7.7", "js-yaml": "^4.1.0", @@ -67,12 +67,12 @@ "react-chartjs-2": "^5.3.0", "react-dom": "19.1.0", "react-error-boundary": "6.0.0", - "react-hook-form": "^7.57.0", - "react-i18next": "15.5.2", + "react-hook-form": "^7.58.1", + "react-i18next": "15.5.3", "react-markdown": "10.1.0", "react-monaco-editor": "0.58.0", "react-router-dom": "7.6.2", - "react-virtuoso": "^4.12.8", + "react-virtuoso": "^4.13.0", "sockette": "^2.0.6", "swr": "^2.3.3", "tar": "^7.4.3", @@ -99,7 +99,7 @@ "prettier": "^3.5.3", "pretty-quick": "^4.2.2", "sass": "^1.89.2", - "terser": "^5.42.0", + "terser": "^5.43.0", "typescript": "^5.8.3", "vite": "^6.3.5", "vite-plugin-monaco-editor": "^1.1.0", diff --git a/clash-verge-rev/pnpm-lock.yaml b/clash-verge-rev/pnpm-lock.yaml index 8b9b1571fa..078f7d22b8 100644 --- a/clash-verge-rev/pnpm-lock.yaml +++ b/clash-verge-rev/pnpm-lock.yaml @@ -36,14 +36,14 @@ importers: specifier: ^7.1.1 version: 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mui/x-data-grid': - specifier: ^8.5.1 - version: 8.5.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^8.5.2 + version: 8.5.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tauri-apps/api': specifier: 2.5.0 version: 2.5.0 '@tauri-apps/plugin-clipboard-manager': - specifier: ^2.2.2 - version: 2.2.2 + specifier: ^2.2.3 + version: 2.2.3 '@tauri-apps/plugin-dialog': specifier: ^2.2.2 version: 2.2.2 @@ -54,20 +54,20 @@ importers: specifier: ^2.2.1 version: 2.2.1 '@tauri-apps/plugin-notification': - specifier: ^2.2.2 - version: 2.2.2 + specifier: ^2.2.3 + version: 2.2.3 '@tauri-apps/plugin-process': - specifier: ^2.2.1 - version: 2.2.1 - '@tauri-apps/plugin-shell': - specifier: 2.2.1 - version: 2.2.1 - '@tauri-apps/plugin-updater': - specifier: 2.7.1 - version: 2.7.1 - '@tauri-apps/plugin-window-state': specifier: ^2.2.2 version: 2.2.2 + '@tauri-apps/plugin-shell': + specifier: 2.2.2 + version: 2.2.2 + '@tauri-apps/plugin-updater': + specifier: 2.8.1 + version: 2.8.1 + '@tauri-apps/plugin-window-state': + specifier: ^2.2.3 + version: 2.2.3 '@types/d3-shape': specifier: ^3.1.7 version: 3.1.7 @@ -78,11 +78,11 @@ importers: specifier: ^3.8.5 version: 3.8.5(react@19.1.0) axios: - specifier: ^1.9.0 - version: 1.9.0 + specifier: ^1.10.0 + version: 1.10.0 chart.js: - specifier: ^4.4.9 - version: 4.4.9 + specifier: ^4.5.0 + version: 4.5.0 cli-color: specifier: ^2.0.4 version: 2.0.4 @@ -96,8 +96,8 @@ importers: specifier: ^0.2.49 version: 0.2.49(react@19.1.0) glob: - specifier: ^11.0.2 - version: 11.0.2 + specifier: ^11.0.3 + version: 11.0.3 i18next: specifier: ^25.2.1 version: 25.2.1(typescript@5.8.3) @@ -127,7 +127,7 @@ importers: version: 19.1.0 react-chartjs-2: specifier: ^5.3.0 - version: 5.3.0(chart.js@4.4.9)(react@19.1.0) + version: 5.3.0(chart.js@4.5.0)(react@19.1.0) react-dom: specifier: 19.1.0 version: 19.1.0(react@19.1.0) @@ -135,11 +135,11 @@ importers: specifier: 6.0.0 version: 6.0.0(react@19.1.0) react-hook-form: - specifier: ^7.57.0 - version: 7.57.0(react@19.1.0) + specifier: ^7.58.1 + version: 7.58.1(react@19.1.0) react-i18next: - specifier: 15.5.2 - version: 15.5.2(i18next@25.2.1(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) + specifier: 15.5.3 + version: 15.5.3(i18next@25.2.1(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) react-markdown: specifier: 10.1.0 version: 10.1.0(@types/react@19.1.8)(react@19.1.0) @@ -150,8 +150,8 @@ importers: specifier: 7.6.2 version: 7.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-virtuoso: - specifier: ^4.12.8 - version: 4.12.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^4.13.0 + version: 4.13.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) sockette: specifier: ^2.0.6 version: 2.0.6 @@ -191,10 +191,10 @@ importers: version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-legacy': specifier: ^6.1.1 - version: 6.1.1(terser@5.42.0)(vite@6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1)) + version: 6.1.1(terser@5.43.0)(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1)) '@vitejs/plugin-react': specifier: 4.5.2 - version: 4.5.2(vite@6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1)) + version: 4.5.2(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1)) adm-zip: specifier: ^0.5.16 version: 0.5.16 @@ -226,20 +226,20 @@ importers: specifier: ^1.89.2 version: 1.89.2 terser: - specifier: ^5.42.0 - version: 5.42.0 + specifier: ^5.43.0 + version: 5.43.0 typescript: specifier: ^5.8.3 version: 5.8.3 vite: specifier: ^6.3.5 - version: 6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1) + version: 6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1) vite-plugin-monaco-editor: specifier: ^1.1.0 version: 1.1.0(monaco-editor@0.52.2) vite-plugin-svgr: specifier: ^4.3.0 - version: 4.3.0(rollup@4.40.2)(typescript@5.8.3)(vite@6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1)) + version: 4.3.0(rollup@4.40.2)(typescript@5.8.3)(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1)) packages: @@ -980,6 +980,14 @@ packages: resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1127,8 +1135,8 @@ packages: '@types/react': optional: true - '@mui/x-data-grid@8.5.1': - resolution: {integrity: sha512-Ukodx8cOc/GR4+2zr4DRNBJJOlyeJNaxK4hggWv7VchDL7Jf70dLZO7oLXPhEFG05Yna81RatL/UFsRYIdWI1Q==} + '@mui/x-data-grid@8.5.2': + resolution: {integrity: sha512-4KzawLZqRKp3KcGKsTDVz7zkEjACllQD5Zb8ds1QKlA6C3/oIoSU7PsemFLj+RL3rT5aORsLMBl97/egQ5tUhA==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.9.0 @@ -1143,8 +1151,8 @@ packages: '@emotion/styled': optional: true - '@mui/x-internals@8.5.1': - resolution: {integrity: sha512-7rAWK7SB6FxEIXKgsHsJjIzeeKOLxFJ16gePgZVWlvyew+xDb4P0fgjwW3ThcJjgvkUm0UhGGfLh/JP8l514IA==} + '@mui/x-internals@8.5.2': + resolution: {integrity: sha512-5YhB2AekK7G8d0YrAjg3WNf0uy3V73JD98WNxJhbIlCraQgl8QOQzr2zNO7MAf/X7mZQtjpjuAsiG3+gI2NVyg==} engines: {node: '>=14.0.0'} peerDependencies: '@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0 @@ -1545,8 +1553,8 @@ packages: engines: {node: '>= 10'} hasBin: true - '@tauri-apps/plugin-clipboard-manager@2.2.2': - resolution: {integrity: sha512-bZvDLMqfcNmsw7Ag8I49jlaCjdpDvvlJHnpp6P+Gg/3xtpSERdwlDxm7cKGbs2mj46dsw4AuG3RoAgcpwgioUA==} + '@tauri-apps/plugin-clipboard-manager@2.2.3': + resolution: {integrity: sha512-myZTLyBpJ9gnDsywtdgRpAYLxEtSVaJa11s1xoiB6w8cjFtG2/znas4Cz3vqYigJkY0A57tyZUE6tjxavIAzgw==} '@tauri-apps/plugin-dialog@2.2.2': resolution: {integrity: sha512-Pm9qnXQq8ZVhAMFSEPwxvh+nWb2mk7LASVlNEHYaksHvcz8P6+ElR5U5dNL9Ofrm+uwhh1/gYKWswK8JJJAh6A==} @@ -1557,20 +1565,20 @@ packages: '@tauri-apps/plugin-global-shortcut@2.2.1': resolution: {integrity: sha512-b64/TI1t5LIi2JY4OWlYjZpPRq60T5GVVL/no27sUuxaNUZY8dVtwsMtDUgxUpln2yR+P2PJsYlqY5V8sLSxEw==} - '@tauri-apps/plugin-notification@2.2.2': - resolution: {integrity: sha512-d71rJdtkFUTcG4dqydnv6d7ZwlNZVcdjrVOPwc9GsF6y9DgVN1WCZ9T/vbfD2qrJslf7ai+rnNJc62TLLC2IdA==} + '@tauri-apps/plugin-notification@2.2.3': + resolution: {integrity: sha512-IlMdSVFsrKg0eIHBloFFosnWbbz6JdwBywfZrYZnE1+acgXvNS3T1YB5w9R6UXw+KKQ94ODBu7JF7a1YUiAK6A==} - '@tauri-apps/plugin-process@2.2.1': - resolution: {integrity: sha512-cF/k8J+YjjuowhNG1AboHNTlrGiOwgX5j6NzsX6WFf9FMzyZUchkCgZMxCdSE5NIgFX0vvOgLQhODFJgbMenLg==} + '@tauri-apps/plugin-process@2.2.2': + resolution: {integrity: sha512-1HuR+uGcokQxlgbS0DheFyMpJSuVGuy3Yh3Eq5o3Jm/sEW+44JaVVgYWM0efpDPA8oT5wpabTFEOZHvKfp8dCg==} - '@tauri-apps/plugin-shell@2.2.1': - resolution: {integrity: sha512-G1GFYyWe/KlCsymuLiNImUgC8zGY0tI0Y3p8JgBCWduR5IEXlIJS+JuG1qtveitwYXlfJrsExt3enhv5l2/yhA==} + '@tauri-apps/plugin-shell@2.2.2': + resolution: {integrity: sha512-fg9XKWfzRQsN8p+Zrk82WeHvXFvGVnG0/mTlujQdLWNnO5cM6WD9qCrHbFytScVS+WhmRAkuypQPcxeKKl3VBg==} - '@tauri-apps/plugin-updater@2.7.1': - resolution: {integrity: sha512-1OPqEY/z7NDVSeTEMIhD2ss/vXWdpfZ5Th2Mk0KtPR/RA6FKuOTDGZQhxoyYBk0pcZJ+nNZUbl/IujDCLBApjA==} + '@tauri-apps/plugin-updater@2.8.1': + resolution: {integrity: sha512-VVQ3wCfM+zok/e0QNBT0oBaFu3gBbzzMsRHzS2yxl7VOCh9dWuZo4yyl29OfaExxSvcixWJ9BZ6pWmKdUt8fCg==} - '@tauri-apps/plugin-window-state@2.2.2': - resolution: {integrity: sha512-7pFwmMtGhhhE/WgmM7PUrj0BSSWVAQMfDdYbRalphIqqF1tWBvxtlxclx8bTutpXHLJTQoCpIeWtBEIXsoAlGw==} + '@tauri-apps/plugin-window-state@2.2.3': + resolution: {integrity: sha512-Iqqzugs6lxpa9JPOe4O33lkCUyMGvh9dqnXof1tK4dP2wU7jKa7W3MLwVyo6c3oVl3dUCm73wkB3RJ0exR0SPg==} '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1705,8 +1713,8 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - axios@1.9.0: - resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} + axios@1.10.0: + resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} babel-plugin-macros@3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} @@ -1730,15 +1738,9 @@ packages: bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -1788,8 +1790,8 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} - chart.js@4.4.9: - resolution: {integrity: sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==} + chart.js@4.5.0: + resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==} engines: {pnpm: '>=8'} chokidar@4.0.3: @@ -2086,8 +2088,8 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - glob@11.0.2: - resolution: {integrity: sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==} + glob@11.0.3: + resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} engines: {node: 20 || >=22} hasBin: true @@ -2205,8 +2207,8 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - jackspeak@4.1.0: - resolution: {integrity: sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==} + jackspeak@4.1.1: + resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} engines: {node: 20 || >=22} js-base64@3.7.7: @@ -2394,8 +2396,8 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - minimatch@10.0.1: - resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} + minimatch@10.0.3: + resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} engines: {node: 20 || >=22} minipass@7.1.2: @@ -2570,14 +2572,14 @@ packages: react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} - react-hook-form@7.57.0: - resolution: {integrity: sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg==} + react-hook-form@7.58.1: + resolution: {integrity: sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-i18next@15.5.2: - resolution: {integrity: sha512-ePODyXgmZQAOYTbZXQn5rRsSBu3Gszo69jxW6aKmlSgxKAI1fOhDwSu6bT4EKHciWPKQ7v7lPrjeiadR6Gi+1A==} + react-i18next@15.5.3: + resolution: {integrity: sha512-ypYmOKOnjqPEJZO4m1BI0kS8kWqkBNsKYyhVUfij0gvjy9xJNoG/VcGkxq5dRlVwzmrmY1BQMAmpbbUBLwC4Kw==} peerDependencies: i18next: '>= 23.2.3' react: '>= 16.8.0' @@ -2638,8 +2640,8 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' - react-virtuoso@4.12.8: - resolution: {integrity: sha512-NMMKfDBr/+xZZqCQF3tN1SZsh6FwOJkYgThlfnsPLkaEhdyQo0EuWUzu3ix6qjnI7rYwJhMwRGoJBi+aiDfGsA==} + react-virtuoso@4.13.0: + resolution: {integrity: sha512-XHv2Fglpx80yFPdjZkV9d1baACKghg/ucpDFEXwaix7z0AfVQj+mF6lM+YQR6UC/TwzXG2rJKydRMb3+7iV3PA==} peerDependencies: react: '>=16 || >=17 || >= 18 || >= 19' react-dom: '>=16 || >=17 || >= 18 || >=19' @@ -2813,8 +2815,8 @@ packages: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} - terser@5.42.0: - resolution: {integrity: sha512-UYCvU9YQW2f/Vwl+P0GfhxJxbUGLwd+5QrrGgLajzWAtC/23AX0vcise32kkP7Eu0Wu9VlzzHAXkLObgjQfFlQ==} + terser@5.43.0: + resolution: {integrity: sha512-CqNNxKSGKSZCunSvwKLTs8u8sGGlp27sxNZ4quGh0QeNuyHM0JSEM/clM9Mf4zUp6J+tO2gUXhgXT2YMMkwfKQ==} engines: {node: '>=10'} hasBin: true @@ -3896,6 +3898,12 @@ snapshots: '@fastify/busboy@2.1.1': {} + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -4038,18 +4046,17 @@ snapshots: optionalDependencies: '@types/react': 19.1.8 - '@mui/x-data-grid@8.5.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@mui/x-data-grid@8.5.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.27.6 '@mui/material': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mui/system': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) '@mui/utils': 7.1.1(@types/react@19.1.8)(react@19.1.0) - '@mui/x-internals': 8.5.1(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) + '@mui/x-internals': 8.5.2(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) clsx: 2.1.1 prop-types: 15.8.1 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - reselect: 5.1.1 use-sync-external-store: 1.5.0(react@19.1.0) optionalDependencies: '@emotion/react': 11.14.0(@types/react@19.1.8)(react@19.1.0) @@ -4057,12 +4064,13 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@mui/x-internals@8.5.1(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)': + '@mui/x-internals@8.5.2(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)': dependencies: '@babel/runtime': 7.27.6 '@mui/system': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) '@mui/utils': 7.1.1(@types/react@19.1.8)(react@19.1.0) react: 19.1.0 + reselect: 5.1.1 transitivePeerDependencies: - '@types/react' @@ -4382,7 +4390,7 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.5.0 '@tauri-apps/cli-win32-x64-msvc': 2.5.0 - '@tauri-apps/plugin-clipboard-manager@2.2.2': + '@tauri-apps/plugin-clipboard-manager@2.2.3': dependencies: '@tauri-apps/api': 2.5.0 @@ -4398,23 +4406,23 @@ snapshots: dependencies: '@tauri-apps/api': 2.5.0 - '@tauri-apps/plugin-notification@2.2.2': + '@tauri-apps/plugin-notification@2.2.3': dependencies: '@tauri-apps/api': 2.5.0 - '@tauri-apps/plugin-process@2.2.1': + '@tauri-apps/plugin-process@2.2.2': dependencies: '@tauri-apps/api': 2.5.0 - '@tauri-apps/plugin-shell@2.2.1': + '@tauri-apps/plugin-shell@2.2.2': dependencies: '@tauri-apps/api': 2.5.0 - '@tauri-apps/plugin-updater@2.7.1': + '@tauri-apps/plugin-updater@2.8.1': dependencies: '@tauri-apps/api': 2.5.0 - '@tauri-apps/plugin-window-state@2.2.2': + '@tauri-apps/plugin-window-state@2.2.3': dependencies: '@tauri-apps/api': 2.5.0 @@ -4499,7 +4507,7 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-legacy@6.1.1(terser@5.42.0)(vite@6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1))': + '@vitejs/plugin-legacy@6.1.1(terser@5.43.0)(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1))': dependencies: '@babel/core': 7.27.4 '@babel/preset-env': 7.27.2(@babel/core@7.27.4) @@ -4509,12 +4517,12 @@ snapshots: magic-string: 0.30.17 regenerator-runtime: 0.14.1 systemjs: 6.15.1 - terser: 5.42.0 - vite: 6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1) + terser: 5.43.0 + vite: 6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.5.2(vite@6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1))': + '@vitejs/plugin-react@4.5.2(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1))': dependencies: '@babel/core': 7.27.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4) @@ -4522,7 +4530,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.11 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1) + vite: 6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1) transitivePeerDependencies: - supports-color @@ -4559,7 +4567,7 @@ snapshots: asynckit@0.4.0: {} - axios@1.9.0: + axios@1.10.0: dependencies: follow-redirects: 1.15.9 form-data: 4.0.2 @@ -4599,14 +4607,8 @@ snapshots: bail@2.0.2: {} - balanced-match@1.0.2: {} - before-after-hook@2.2.3: {} - brace-expansion@2.0.1: - dependencies: - balanced-match: 1.0.2 - braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -4647,7 +4649,7 @@ snapshots: character-reference-invalid@2.0.1: {} - chart.js@4.4.9: + chart.js@4.5.0: dependencies: '@kurkle/color': 0.3.4 @@ -4954,11 +4956,11 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - glob@11.0.2: + glob@11.0.3: dependencies: foreground-child: 3.3.1 - jackspeak: 4.1.0 - minimatch: 10.0.1 + jackspeak: 4.1.1 + minimatch: 10.0.3 minipass: 7.1.2 package-json-from-dist: 1.0.1 path-scurry: 2.0.0 @@ -5075,7 +5077,7 @@ snapshots: isexe@2.0.0: {} - jackspeak@4.1.0: + jackspeak@4.1.1: dependencies: '@isaacs/cliui': 8.0.2 @@ -5382,9 +5384,9 @@ snapshots: dependencies: mime-db: 1.52.0 - minimatch@10.0.1: + minimatch@10.0.3: dependencies: - brace-expansion: 2.0.1 + '@isaacs/brace-expansion': 5.0.0 minipass@7.1.2: {} @@ -5539,9 +5541,9 @@ snapshots: proxy-from-env@1.1.0: {} - react-chartjs-2@5.3.0(chart.js@4.4.9)(react@19.1.0): + react-chartjs-2@5.3.0(chart.js@4.5.0)(react@19.1.0): dependencies: - chart.js: 4.4.9 + chart.js: 4.5.0 react: 19.1.0 react-dom@19.1.0(react@19.1.0): @@ -5556,11 +5558,11 @@ snapshots: react-fast-compare@3.2.2: {} - react-hook-form@7.57.0(react@19.1.0): + react-hook-form@7.58.1(react@19.1.0): dependencies: react: 19.1.0 - react-i18next@15.5.2(i18next@25.2.1(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3): + react-i18next@15.5.3(i18next@25.2.1(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3): dependencies: '@babel/runtime': 7.27.6 html-parse-stringify: 3.0.1 @@ -5623,7 +5625,7 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - react-virtuoso@4.12.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + react-virtuoso@4.13.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) @@ -5816,7 +5818,7 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 - terser@5.42.0: + terser@5.43.0: dependencies: '@jridgewell/source-map': 0.3.6 acorn: 8.14.1 @@ -5928,18 +5930,18 @@ snapshots: dependencies: monaco-editor: 0.52.2 - vite-plugin-svgr@4.3.0(rollup@4.40.2)(typescript@5.8.3)(vite@6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1)): + vite-plugin-svgr@4.3.0(rollup@4.40.2)(typescript@5.8.3)(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1)): dependencies: '@rollup/pluginutils': 5.1.4(rollup@4.40.2) '@svgr/core': 8.1.0(typescript@5.8.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3)) - vite: 6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1) + vite: 6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1) transitivePeerDependencies: - rollup - supports-color - typescript - vite@6.3.5(sass@1.89.2)(terser@5.42.0)(yaml@2.7.1): + vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1): dependencies: esbuild: 0.25.4 fdir: 6.4.4(picomatch@4.0.2) @@ -5950,7 +5952,7 @@ snapshots: optionalDependencies: fsevents: 2.3.3 sass: 1.89.2 - terser: 5.42.0 + terser: 5.43.0 yaml: 2.7.1 void-elements@3.1.0: {} diff --git a/clash-verge-rev/scripts/prepare.mjs b/clash-verge-rev/scripts/prebuild.mjs similarity index 100% rename from clash-verge-rev/scripts/prepare.mjs rename to clash-verge-rev/scripts/prebuild.mjs diff --git a/clash-verge-rev/src-tauri/Cargo.lock b/clash-verge-rev/src-tauri/Cargo.lock index e79f7877b5..b17f5a822b 100644 --- a/clash-verge-rev/src-tauri/Cargo.lock +++ b/clash-verge-rev/src-tauri/Cargo.lock @@ -1042,7 +1042,7 @@ dependencies = [ [[package]] name = "clash-verge" -version = "2.3.0" +version = "2.3.1" dependencies = [ "ab_glyph", "aes-gcm", @@ -1100,13 +1100,13 @@ dependencies = [ "tauri-plugin-window-state", "tempfile", "tokio", - "tokio-tungstenite 0.26.2", - "tungstenite 0.26.2", + "tokio-tungstenite 0.27.0", + "tungstenite 0.27.0", "users", "warp", "winapi", "winreg 0.55.0", - "zip 4.0.0", + "zip", ] [[package]] @@ -3587,9 +3587,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.172" +version = "0.2.173" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" [[package]] name = "libfuzzer-sys" @@ -6977,8 +6977,9 @@ dependencies = [ [[package]] name = "tauri-plugin-autostart" -version = "2.3.0" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#9bc4b2230ebb32bd30a4c0c2a21077829a729193" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9641831518c56775a364a8750e0eed8852adee87e0f11006d043b9ebba0bf5" dependencies = [ "auto-launch", "serde", @@ -6990,9 +6991,9 @@ dependencies = [ [[package]] name = "tauri-plugin-clipboard-manager" -version = "2.2.2" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab4cb42fdf745229b768802e9180920a4be63122cf87ed1c879103f7609d98e" +checksum = "11fa4f17a6d380490597f7632aca40b65d379cb374cb92bd9d80f333309b7fd7" dependencies = [ "arboard", "log", @@ -7107,9 +7108,9 @@ dependencies = [ [[package]] name = "tauri-plugin-process" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57da5888533e802b6206b9685091f8714aa1f5266dc80051a82388449558b773" +checksum = "4d870adae9408be585abd56eade2b5def2660339512b7c8de5ddf21238b67a34" dependencies = [ "tauri", "tauri-plugin", @@ -7117,9 +7118,9 @@ dependencies = [ [[package]] name = "tauri-plugin-shell" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d5eb3368b959937ad2aeaf6ef9a8f5d11e01ffe03629d3530707bbcb27ff5d" +checksum = "d34e525a448b80ad5d906fcbd93838ac3ba37985b29ac699a045b5da9b0a1a22" dependencies = [ "encoding_rs", "log", @@ -7138,9 +7139,9 @@ dependencies = [ [[package]] name = "tauri-plugin-updater" -version = "2.7.1" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f05c38afd77a4b8fd98e8fb6f1cdbb5fbb8a46ba181eb2758b05321e3c6209" +checksum = "b068673e9037376ca9906f99b00ae5f9e6eb62f456f900b4435c38d57cfa73e4" dependencies = [ "base64 0.22.1", "dirs 6.0.0", @@ -7164,15 +7165,15 @@ dependencies = [ "time", "tokio", "url", - "windows-sys 0.59.0", - "zip 2.6.1", + "windows-sys 0.60.2", + "zip", ] [[package]] name = "tauri-plugin-window-state" -version = "2.2.2" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27a3fe49de72adbe0d84aee33c89a0b059722cd0b42aaeab29eaaee7f7535cd" +checksum = "136e5ce5e61edc8eeeaca70080811bbdcdd890cac9c4070cb4db9cc3de1da449" dependencies = [ "bitflags 2.9.0", "log", @@ -7595,14 +7596,14 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.26.2" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1" dependencies = [ "futures-util", "log", "tokio", - "tungstenite 0.26.2", + "tungstenite 0.27.0", ] [[package]] @@ -7953,9 +7954,9 @@ dependencies = [ [[package]] name = "tungstenite" -version = "0.26.2" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" dependencies = [ "bytes", "data-encoding", @@ -8870,6 +8871,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -8909,13 +8919,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows-version" version = "0.1.4" @@ -8943,6 +8969,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -8961,6 +8993,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -8979,12 +9017,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -9003,6 +9053,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -9021,6 +9077,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -9039,6 +9101,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -9057,6 +9125,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" version = "0.5.40" @@ -9452,22 +9526,9 @@ dependencies = [ [[package]] name = "zip" -version = "2.6.1" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744" -dependencies = [ - "arbitrary", - "crc32fast", - "crossbeam-utils", - "indexmap 2.8.0", - "memchr", -] - -[[package]] -name = "zip" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "153a6fff49d264c4babdcfa6b4d534747f520e56e8f0f384f3b808c4b64cc1fd" +checksum = "af7dcdb4229c0e79c2531a24de7726a0e980417a74fb4d030a35f535665439a0" dependencies = [ "aes", "arbitrary", diff --git a/clash-verge-rev/src-tauri/Cargo.toml b/clash-verge-rev/src-tauri/Cargo.toml index d9c7d18bda..080451f875 100755 --- a/clash-verge-rev/src-tauri/Cargo.toml +++ b/clash-verge-rev/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "clash-verge" -version = "2.3.0" +version = "2.3.1" description = "clash verge" authors = ["zzzgydi", "wonfen", "MystiPanda"] license = "GPL-3.0-only" @@ -55,27 +55,27 @@ tauri = { version = "2.5.1", features = [ "image-png", ] } network-interface = { version = "2.0.1", features = ["serde"] } -tauri-plugin-shell = "2.2.1" +tauri-plugin-shell = "2.2.2" tauri-plugin-dialog = "2.2.2" tauri-plugin-fs = "2.3.0" -tauri-plugin-process = "2.2.1" -tauri-plugin-clipboard-manager = "2.2.2" +tauri-plugin-process = "2.2.2" +tauri-plugin-clipboard-manager = "2.2.3" tauri-plugin-deep-link = "2.3.0" tauri-plugin-devtools = "2.0.0" -tauri-plugin-window-state = "2.2.2" -zip = "4.0.0" +tauri-plugin-window-state = "2.2.3" +zip = "4.1.0" reqwest_dav = "0.2.1" aes-gcm = { version = "0.10.3", features = ["std"] } base64 = "0.22.1" getrandom = "0.3.3" -tokio-tungstenite = "0.26.2" +tokio-tungstenite = "0.27.0" futures = "0.3.31" sys-locale = "0.3.2" async-trait = "0.1.88" mihomo_api = { path = "src_crates/crate_mihomo_api" } ab_glyph = "0.2.29" -tungstenite = "0.26.2" -libc = "0.2.172" +tungstenite = "0.27.0" +libc = "0.2.173" gethostname = "1.0.2" hmac = "0.12.1" sha2 = "0.10.9" @@ -99,9 +99,9 @@ winapi = { version = "0.3.9", features = [ users = "0.11.0" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] -tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +tauri-plugin-autostart = "2.4.0" tauri-plugin-global-shortcut = "2.2.1" -tauri-plugin-updater = "2.7.1" +tauri-plugin-updater = "2.8.1" [features] default = ["custom-protocol"] diff --git a/clash-verge-rev/src-tauri/packages/windows/installer.nsi b/clash-verge-rev/src-tauri/packages/windows/installer.nsi index 35d7eb9636..15a2689e24 100644 --- a/clash-verge-rev/src-tauri/packages/windows/installer.nsi +++ b/clash-verge-rev/src-tauri/packages/windows/installer.nsi @@ -916,9 +916,9 @@ FunctionEnd !macroend Section Uninstall - ;删除 .window-state.json 文件 + ;删除 window-state.json 文件 SetShellVarContext current - Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json" + Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json" !insertmacro CheckIfAppIsRunning !insertmacro CheckAllVergeProcesses @@ -1015,9 +1015,9 @@ Section Uninstall RmDir /r "$LOCALAPPDATA\${BUNDLEID}" ${EndIf} - ;删除 .window-state.json 文件 + ;删除 window-state.json 文件 SetShellVarContext current - Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json" + Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json" ${GetOptions} $CMDLINE "/P" $R0 IfErrors +2 0 diff --git a/clash-verge-rev/src-tauri/src/cmd/profile.rs b/clash-verge-rev/src-tauri/src/cmd/profile.rs index f1ecab6a3b..36de59970e 100644 --- a/clash-verge-rev/src-tauri/src/cmd/profile.rs +++ b/clash-verge-rev/src-tauri/src/cmd/profile.rs @@ -12,45 +12,89 @@ use tokio::sync::Mutex; // 添加全局互斥锁防止并发配置更新 static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(()); -/// 获取配置文件列表 +/// 获取配置文件避免锁竞争 #[tauri::command] pub async fn get_profiles() -> CmdResult { - let profiles_result = tokio::time::timeout( - Duration::from_secs(3), // 3秒超时 - tokio::task::spawn_blocking(move || Config::profiles().data().clone()), + // 策略1: 尝试快速获取latest数据 + let latest_result = tokio::time::timeout( + Duration::from_millis(500), + tokio::task::spawn_blocking(move || { + let profiles = Config::profiles(); + let latest = profiles.latest(); + IProfiles { + current: latest.current.clone(), + items: latest.items.clone(), + } + }), ) .await; - match profiles_result { - Ok(Ok(profiles)) => Ok(*profiles), + match latest_result { + Ok(Ok(profiles)) => { + logging!(info, Type::Cmd, false, "快速获取配置列表成功"); + return Ok(profiles); + } Ok(Err(join_err)) => { - logging!(error, Type::Cmd, true, "获取配置列表任务失败: {}", join_err); - Ok(IProfiles { - current: None, - items: Some(vec![]), - }) + logging!(warn, Type::Cmd, true, "快速获取配置任务失败: {}", join_err); } Err(_) => { - // 超时情况 + logging!(warn, Type::Cmd, true, "快速获取配置超时(500ms)"); + } + } + + // 策略2: 如果快速获取失败,尝试获取data() + let data_result = tokio::time::timeout( + Duration::from_secs(2), + tokio::task::spawn_blocking(move || { + let profiles = Config::profiles(); + let data = profiles.data(); + IProfiles { + current: data.current.clone(), + items: data.items.clone(), + } + }), + ) + .await; + + match data_result { + Ok(Ok(profiles)) => { + logging!(info, Type::Cmd, false, "获取draft配置列表成功"); + return Ok(profiles); + } + Ok(Err(join_err)) => { logging!( error, Type::Cmd, true, - "获取配置列表超时(3秒),可能存在锁竞争" + "获取draft配置任务失败: {}", + join_err ); - match tokio::task::spawn_blocking(move || Config::profiles().latest().clone()).await { - Ok(profiles) => { - logging!(info, Type::Cmd, true, "使用latest()成功获取配置"); - Ok(*profiles) - } - Err(_) => { - logging!(error, Type::Cmd, true, "fallback获取配置也失败,返回空配置"); - Ok(IProfiles { - current: None, - items: Some(vec![]), - }) - } - } + } + Err(_) => { + logging!(error, Type::Cmd, true, "获取draft配置超时(2秒)"); + } + } + + // 策略3: fallback,尝试重新创建配置 + logging!( + warn, + Type::Cmd, + true, + "所有获取配置策略都失败,尝试fallback" + ); + + match tokio::task::spawn_blocking(IProfiles::new).await { + Ok(profiles) => { + logging!(info, Type::Cmd, true, "使用fallback配置成功"); + Ok(profiles) + } + Err(err) => { + logging!(error, Type::Cmd, true, "fallback配置也失败: {}", err); + // 返回空配置避免崩溃 + Ok(IProfiles { + current: None, + items: Some(vec![]), + }) } } } @@ -246,6 +290,13 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { Config::profiles().apply(); handle::Handle::refresh_clash(); + // 强制刷新代理缓存,确保profile切换后立即获取最新节点数据 + crate::process::AsyncHandler::spawn(|| async move { + if let Err(e) = super::proxy::force_refresh_proxies().await { + log::warn!(target: "app", "强制刷新代理缓存失败: {}", e); + } + }); + crate::process::AsyncHandler::spawn(|| async move { if let Err(e) = Tray::global().update_tooltip() { log::warn!(target: "app", "异步更新托盘提示失败: {}", e); diff --git a/clash-verge-rev/src-tauri/src/cmd/proxy.rs b/clash-verge-rev/src-tauri/src/cmd/proxy.rs index c6b58a0352..48be7c2cea 100644 --- a/clash-verge-rev/src-tauri/src/cmd/proxy.rs +++ b/clash-verge-rev/src-tauri/src/cmd/proxy.rs @@ -43,6 +43,28 @@ pub async fn get_proxies() -> CmdResult { Ok(*proxies) } +/// 强制刷新代理缓存用于profile切换 +#[tauri::command] +pub async fn force_refresh_proxies() -> CmdResult { + let manager = MihomoManager::global(); + let app_handle = handle::Handle::global().app_handle().unwrap(); + let cmd_proxy_state = app_handle.state::>(); + + log::debug!(target: "app", "强制刷新代理缓存"); + + let proxies = manager.get_refresh_proxies().await?; + + { + let mut state = cmd_proxy_state.lock().unwrap(); + state.proxies = Box::new(proxies.clone()); + state.need_refresh = false; + state.last_refresh_time = Instant::now(); + } + + log::debug!(target: "app", "强制刷新代理缓存完成"); + Ok(proxies) +} + #[tauri::command] pub async fn get_providers_proxies() -> CmdResult { let app_handle = handle::Handle::global().app_handle().unwrap(); diff --git a/clash-verge-rev/src-tauri/src/config/verge.rs b/clash-verge-rev/src-tauri/src/config/verge.rs index 603fed2a40..9510228eb7 100644 --- a/clash-verge-rev/src-tauri/src/config/verge.rs +++ b/clash-verge-rev/src-tauri/src/config/verge.rs @@ -296,7 +296,6 @@ impl IVerge { } /// 配置修正后重新加载配置 - fn reload_config_after_fix(updated_config: IVerge) -> Result<()> { use crate::config::Config; diff --git a/clash-verge-rev/src-tauri/src/core/handle.rs b/clash-verge-rev/src-tauri/src/core/handle.rs index 0ab9faee84..a3c4a74e11 100644 --- a/clash-verge-rev/src-tauri/src/core/handle.rs +++ b/clash-verge-rev/src-tauri/src/core/handle.rs @@ -151,6 +151,10 @@ impl NotificationSystem { match window.emit(event_name_str, payload) { Ok(_) => { system.stats.total_sent.fetch_add(1, Ordering::SeqCst); + // 记录成功发送的事件 + if log::log_enabled!(log::Level::Debug) { + log::debug!("Successfully emitted event: {}", event_name_str); + } } Err(e) => { log::warn!("Failed to emit event {}: {}", event_name_str, e); @@ -224,12 +228,27 @@ impl NotificationSystem { } fn shutdown(&mut self) { + log::info!("NotificationSystem shutdown initiated"); self.is_running = false; - self.sender = None; - if let Some(handle) = self.worker_handle.take() { - let _ = handle.join(); + // 先关闭发送端,让接收端知道不会再有新消息 + if let Some(sender) = self.sender.take() { + drop(sender); } + + // 设置超时避免无限等待 + if let Some(handle) = self.worker_handle.take() { + match handle.join() { + Ok(_) => { + log::info!("NotificationSystem worker thread joined successfully"); + } + Err(e) => { + log::error!("NotificationSystem worker thread join failed: {:?}", e); + } + } + } + + log::info!("NotificationSystem shutdown completed"); } } diff --git a/clash-verge-rev/src-tauri/src/core/hotkey.rs b/clash-verge-rev/src-tauri/src/core/hotkey.rs index b8f87d8fbd..070a049bbe 100755 --- a/clash-verge-rev/src-tauri/src/core/hotkey.rs +++ b/clash-verge-rev/src-tauri/src/core/hotkey.rs @@ -272,10 +272,11 @@ impl Hotkey { if is_enable_global_hotkey { f(); - } else if let Some(window) = app_handle.get_webview_window("main") { + } else { + use crate::utils::window_manager::WindowManager; // 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键 - let is_visible = window.is_visible().unwrap_or(false); - let is_focused = window.is_focused().unwrap_or(false); + let is_visible = WindowManager::is_main_window_visible(); + let is_focused = WindowManager::is_main_window_focused(); if is_focused && is_visible { f(); @@ -330,9 +331,9 @@ impl Hotkey { let func = iter.next(); let key = iter.next(); - if func.is_some() && key.is_some() { - let func = func.unwrap().trim(); - let key = key.unwrap().trim(); + if let (Some(func), Some(key)) = (func, key) { + let func = func.trim(); + let key = key.trim(); map.insert(key, func); } }); diff --git a/clash-verge-rev/src-tauri/src/core/tray/mod.rs b/clash-verge-rev/src-tauri/src/core/tray/mod.rs index e5431ef49e..527649171c 100644 --- a/clash-verge-rev/src-tauri/src/core/tray/mod.rs +++ b/clash-verge-rev/src-tauri/src/core/tray/mod.rs @@ -10,7 +10,6 @@ use crate::{ lightweight::{entry_lightweight_mode, is_in_lightweight_mode}, mihomo::Rate, }, - resolve, utils::{dirs::find_target_icons, i18n::t, resolve::VERSION}, Type, }; @@ -653,10 +652,14 @@ impl Tray { "system_proxy" => feat::toggle_system_proxy(), "tun_mode" => feat::toggle_tun_mode(None), "main_window" => { + use crate::utils::window_manager::WindowManager; + log::info!(target: "app", "Tray点击事件: 显示主窗口"); if crate::module::lightweight::is_in_lightweight_mode() { + log::info!(target: "app", "当前在轻量模式,正在退出轻量模式"); crate::module::lightweight::exit_lightweight_mode(); } - let _ = resolve::create_window(true); + let result = WindowManager::show_main_window(); + log::info!(target: "app", "窗口显示结果: {:?}", result); } _ => {} } @@ -917,12 +920,16 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { feat::change_clash_mode(mode.into()); } "open_window" => { + use crate::utils::window_manager::WindowManager; + log::info!(target: "app", "托盘菜单点击: 打开窗口"); // 如果在轻量模式中,先退出轻量模式 if crate::module::lightweight::is_in_lightweight_mode() { + log::info!(target: "app", "当前在轻量模式,正在退出"); crate::module::lightweight::exit_lightweight_mode(); } - // 然后创建窗口 - let _ = resolve::create_window(true); + // 使用统一的窗口管理器显示窗口 + let result = WindowManager::show_main_window(); + log::info!(target: "app", "窗口显示结果: {:?}", result); } "system_proxy" => feat::toggle_system_proxy(), "tun_mode" => feat::toggle_tun_mode(None), diff --git a/clash-verge-rev/src-tauri/src/feat/window.rs b/clash-verge-rev/src-tauri/src/feat/window.rs index d537d8061c..4ac7505818 100644 --- a/clash-verge-rev/src-tauri/src/feat/window.rs +++ b/clash-verge-rev/src-tauri/src/feat/window.rs @@ -5,53 +5,29 @@ use crate::{ core::{handle, sysopt, CoreManager}, logging, module::mihomo::MihomoManager, - utils::{logging::Type, resolve}, + utils::logging::Type, }; /// Open or close the dashboard window #[allow(dead_code)] pub fn open_or_close_dashboard() { + use crate::utils::window_manager::WindowManager; + log::info!(target: "app", "Attempting to open/close dashboard"); // 检查是否在轻量模式下 if crate::module::lightweight::is_in_lightweight_mode() { log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode"); - crate::module::lightweight::exit_lightweight_mode(); - log::info!(target: "app", "Creating new window after exiting lightweight mode"); - resolve::create_window(true); + let result = WindowManager::show_main_window(); + log::info!(target: "app", "Window operation result: {:?}", result); return; } - if let Some(window) = handle::Handle::global().get_window() { - log::info!(target: "app", "Found existing window"); - - // 如果窗口存在,则切换其显示状态 - match window.is_visible() { - Ok(visible) => { - log::info!(target: "app", "Window visibility status: {}", visible); - - if visible { - log::info!(target: "app", "Attempting to hide window"); - let _ = window.hide(); - } else { - log::info!(target: "app", "Attempting to show and focus window"); - if window.is_minimized().unwrap_or(false) { - let _ = window.unminimize(); - } - let _ = window.show(); - let _ = window.set_focus(); - } - } - Err(e) => { - log::error!(target: "app", "Failed to get window visibility: {:?}", e); - } - } - } else { - log::info!(target: "app", "No existing window found, creating new window"); - resolve::create_window(true); - } + // 使用统一的窗口管理器切换窗口状态 + let result = WindowManager::toggle_main_window(); + log::info!(target: "app", "Window toggle result: {:?}", result); } /// 异步优化的应用退出函数 @@ -154,7 +130,12 @@ async fn clean_async() -> bool { // 4. DNS恢复(仅macOS) #[cfg(target_os = "macos")] let dns_task = async { - match timeout(Duration::from_millis(1000), resolve::restore_public_dns()).await { + match timeout( + Duration::from_millis(1000), + crate::utils::resolve::restore_public_dns(), + ) + .await + { Ok(_) => { log::info!(target: "app", "DNS设置已恢复"); true diff --git a/clash-verge-rev/src-tauri/src/lib.rs b/clash-verge-rev/src-tauri/src/lib.rs index 245ad23c77..961d898cac 100644 --- a/clash-verge-rev/src-tauri/src/lib.rs +++ b/clash-verge-rev/src-tauri/src/lib.rs @@ -162,6 +162,14 @@ pub fn run() { }); }); + // 窗口管理 + logging!(info, Type::Setup, true, "初始化窗口状态管理..."); + let window_state_plugin = tauri_plugin_window_state::Builder::new() + .with_filename("window_state.json") + .with_state_flags(tauri_plugin_window_state::StateFlags::default()) + .build(); + let _ = app.handle().plugin(window_state_plugin); + // 异步处理 let app_handle = app.handle().clone(); AsyncHandler::spawn(move || async move { @@ -255,6 +263,7 @@ pub fn run() { cmd::invoke_uwp_tool, cmd::copy_clash_env, cmd::get_proxies, + cmd::force_refresh_proxies, cmd::get_providers_proxies, cmd::save_dns_config, cmd::apply_dns_config, diff --git a/clash-verge-rev/src-tauri/src/module/lightweight.rs b/clash-verge-rev/src-tauri/src/module/lightweight.rs index 529d006be3..327dd7d8ff 100644 --- a/clash-verge-rev/src-tauri/src/module/lightweight.rs +++ b/clash-verge-rev/src-tauri/src/module/lightweight.rs @@ -93,10 +93,18 @@ pub fn disable_auto_light_weight_mode() { } pub fn entry_lightweight_mode() { + use crate::utils::window_manager::WindowManager; + + let result = WindowManager::hide_main_window(); + logging!( + info, + Type::Lightweight, + true, + "轻量模式隐藏窗口结果: {:?}", + result + ); + if let Some(window) = handle::Handle::global().get_window() { - if window.is_visible().unwrap_or(false) { - let _ = window.hide(); - } if let Some(webview) = window.get_webview_window("main") { let _ = webview.destroy(); } diff --git a/clash-verge-rev/src-tauri/src/utils/mod.rs b/clash-verge-rev/src-tauri/src/utils/mod.rs index 6a0a09fb53..3f7f6aa09a 100644 --- a/clash-verge-rev/src-tauri/src/utils/mod.rs +++ b/clash-verge-rev/src-tauri/src/utils/mod.rs @@ -8,3 +8,4 @@ pub mod network; pub mod resolve; pub mod server; pub mod tmpl; +pub mod window_manager; diff --git a/clash-verge-rev/src-tauri/src/utils/resolve.rs b/clash-verge-rev/src-tauri/src/utils/resolve.rs index 5af3b56a37..8e55585835 100644 --- a/clash-verge-rev/src-tauri/src/utils/resolve.rs +++ b/clash-verge-rev/src-tauri/src/utils/resolve.rs @@ -307,8 +307,17 @@ pub fn create_window(is_show: bool) -> bool { if let Some(window) = app_handle.get_webview_window("main") { logging!(info, Type::Window, true, "主窗口已存在,将显示现有窗口"); if is_show { + if window.is_minimized().unwrap_or(false) { + logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化"); + let _ = window.unminimize(); + } let _ = window.show(); let _ = window.set_focus(); + + #[cfg(target_os = "macos")] + { + AppHandleManager::global().set_activation_policy_regular(); + } } return true; } diff --git a/clash-verge-rev/src-tauri/src/utils/window_manager.rs b/clash-verge-rev/src-tauri/src/utils/window_manager.rs new file mode 100644 index 0000000000..d00656f647 --- /dev/null +++ b/clash-verge-rev/src-tauri/src/utils/window_manager.rs @@ -0,0 +1,272 @@ +use crate::{core::handle, logging, utils::logging::Type}; +use tauri::{Manager, WebviewWindow, Wry}; + +#[cfg(target_os = "macos")] +use crate::AppHandleManager; + +/// 窗口操作结果 +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum WindowOperationResult { + /// 窗口已显示并获得焦点 + Shown, + /// 窗口已隐藏 + Hidden, + /// 创建了新窗口 + Created, + /// 操作失败 + Failed, + /// 无需操作 + NoAction, +} + +/// 窗口状态 +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum WindowState { + /// 窗口可见且有焦点 + VisibleFocused, + /// 窗口可见但无焦点 + VisibleUnfocused, + /// 窗口最小化 + Minimized, + /// 窗口隐藏 + Hidden, + /// 窗口不存在 + NotExist, +} + +/// 统一的窗口管理器 +pub struct WindowManager; + +impl WindowManager { + pub fn get_main_window_state() -> WindowState { + if let Some(window) = Self::get_main_window() { + if window.is_minimized().unwrap_or(false) { + WindowState::Minimized + } else if window.is_visible().unwrap_or(false) { + if window.is_focused().unwrap_or(false) { + WindowState::VisibleFocused + } else { + WindowState::VisibleUnfocused + } + } else { + WindowState::Hidden + } + } else { + WindowState::NotExist + } + } + + /// 获取主窗口实例 + pub fn get_main_window() -> Option> { + handle::Handle::global() + .app_handle() + .and_then(|app| app.get_webview_window("main")) + } + + /// 智能显示主窗口 + pub fn show_main_window() -> WindowOperationResult { + logging!(info, Type::Window, true, "开始智能显示主窗口"); + logging!( + debug, + Type::Window, + true, + "{}", + Self::get_window_status_info() + ); + + let current_state = Self::get_main_window_state(); + + match current_state { + WindowState::NotExist => { + logging!(info, Type::Window, true, "窗口不存在,创建新窗口"); + if Self::create_new_window() { + WindowOperationResult::Created + } else { + WindowOperationResult::Failed + } + } + WindowState::VisibleFocused => { + logging!(info, Type::Window, true, "窗口已经可见且有焦点,无需操作"); + WindowOperationResult::NoAction + } + WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => { + if let Some(window) = Self::get_main_window() { + Self::activate_window(&window) + } else { + WindowOperationResult::Failed + } + } + } + } + + /// 切换主窗口显示状态(显示/隐藏) + pub fn toggle_main_window() -> WindowOperationResult { + logging!(info, Type::Window, true, "开始切换主窗口显示状态"); + + let current_state = Self::get_main_window_state(); + logging!( + info, + Type::Window, + true, + "当前窗口状态: {:?}", + current_state + ); + + match current_state { + WindowState::NotExist => { + // 窗口不存在,创建新窗口 + if Self::create_new_window() { + WindowOperationResult::Created + } else { + WindowOperationResult::Failed + } + } + WindowState::VisibleFocused => { + // 窗口可见且有焦点,隐藏它 + if let Some(window) = Self::get_main_window() { + if window.hide().is_ok() { + logging!(info, Type::Window, true, "窗口已隐藏"); + WindowOperationResult::Hidden + } else { + WindowOperationResult::Failed + } + } else { + WindowOperationResult::Failed + } + } + WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => { + // 窗口存在但不可见或无焦点,激活它 + if let Some(window) = Self::get_main_window() { + Self::activate_window(&window) + } else { + WindowOperationResult::Failed + } + } + } + } + + /// 激活窗口(取消最小化、显示、设置焦点) + fn activate_window(window: &WebviewWindow) -> WindowOperationResult { + logging!(info, Type::Window, true, "开始激活窗口"); + + let mut operations_successful = true; + + // 1. 如果窗口最小化,先取消最小化 + if window.is_minimized().unwrap_or(false) { + logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化"); + if let Err(e) = window.unminimize() { + logging!(warn, Type::Window, true, "取消最小化失败: {}", e); + operations_successful = false; + } + } + + // 2. 显示窗口 + if let Err(e) = window.show() { + logging!(warn, Type::Window, true, "显示窗口失败: {}", e); + operations_successful = false; + } + + // 3. 设置焦点 + if let Err(e) = window.set_focus() { + logging!(warn, Type::Window, true, "设置窗口焦点失败: {}", e); + operations_successful = false; + } + + // 4. 平台特定的激活策略 + #[cfg(target_os = "macos")] + { + logging!(info, Type::Window, true, "应用 macOS 特定的激活策略"); + AppHandleManager::global().set_activation_policy_regular(); + } + + #[cfg(target_os = "windows")] + { + // Windows 尝试额外的激活方法 + if let Err(e) = window.set_always_on_top(true) { + logging!( + debug, + Type::Window, + true, + "设置置顶失败(非关键错误): {}", + e + ); + } + // 立即取消置顶 + if let Err(e) = window.set_always_on_top(false) { + logging!( + debug, + Type::Window, + true, + "取消置顶失败(非关键错误): {}", + e + ); + } + } + + if operations_successful { + logging!(info, Type::Window, true, "窗口激活成功"); + WindowOperationResult::Shown + } else { + logging!(warn, Type::Window, true, "窗口激活部分失败"); + WindowOperationResult::Failed + } + } + + /// 隐藏主窗口 + pub fn hide_main_window() -> WindowOperationResult { + logging!(info, Type::Window, true, "开始隐藏主窗口"); + + if let Some(window) = Self::get_main_window() { + if window.hide().is_ok() { + logging!(info, Type::Window, true, "窗口已隐藏"); + WindowOperationResult::Hidden + } else { + logging!(warn, Type::Window, true, "隐藏窗口失败"); + WindowOperationResult::Failed + } + } else { + logging!(info, Type::Window, true, "窗口不存在,无需隐藏"); + WindowOperationResult::NoAction + } + } + + /// 检查窗口是否可见 + pub fn is_main_window_visible() -> bool { + Self::get_main_window() + .map(|window| window.is_visible().unwrap_or(false)) + .unwrap_or(false) + } + + /// 检查窗口是否有焦点 + pub fn is_main_window_focused() -> bool { + Self::get_main_window() + .map(|window| window.is_focused().unwrap_or(false)) + .unwrap_or(false) + } + + /// 检查窗口是否最小化 + pub fn is_main_window_minimized() -> bool { + Self::get_main_window() + .map(|window| window.is_minimized().unwrap_or(false)) + .unwrap_or(false) + } + + /// 创建新窗口现有的实现 + fn create_new_window() -> bool { + use crate::utils::resolve; + resolve::create_window(true) + } + + /// 获取详细的窗口状态信息 + pub fn get_window_status_info() -> String { + let state = Self::get_main_window_state(); + let is_visible = Self::is_main_window_visible(); + let is_focused = Self::is_main_window_focused(); + let is_minimized = Self::is_main_window_minimized(); + + format!( + "窗口状态: {:?} | 可见: {} | 有焦点: {} | 最小化: {}", + state, is_visible, is_focused, is_minimized + ) + } +} diff --git a/clash-verge-rev/src-tauri/tauri.conf.json b/clash-verge-rev/src-tauri/tauri.conf.json index 04f10958dd..894d62a280 100755 --- a/clash-verge-rev/src-tauri/tauri.conf.json +++ b/clash-verge-rev/src-tauri/tauri.conf.json @@ -1,5 +1,5 @@ { - "version": "2.3.0", + "version": "2.3.1", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "bundle": { "active": true, diff --git a/clash-verge-rev/src/hooks/use-profiles.ts b/clash-verge-rev/src/hooks/use-profiles.ts index d8f7f04a73..f94aceb942 100644 --- a/clash-verge-rev/src/hooks/use-profiles.ts +++ b/clash-verge-rev/src/hooks/use-profiles.ts @@ -10,11 +10,32 @@ export const useProfiles = () => { const { data: profiles, mutate: mutateProfiles } = useSWR( "getProfiles", getProfiles, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + dedupingInterval: 2000, + errorRetryCount: 2, + errorRetryInterval: 1000, + }, ); const patchProfiles = async (value: Partial) => { - await patchProfilesConfig(value); - mutateProfiles(); + // 立即更新本地状态 + if (value.current && profiles) { + const optimisticUpdate = { + ...profiles, + current: value.current, + }; + mutateProfiles(optimisticUpdate, false); // 不重新验证 + } + + try { + await patchProfilesConfig(value); + mutateProfiles(); + } catch (error) { + mutateProfiles(); + throw error; + } }; const patchCurrent = async (value: Partial) => { @@ -26,40 +47,90 @@ export const useProfiles = () => { // 根据selected的节点选择 const activateSelected = async () => { - const proxiesData = await getProxies(); - const profileData = await getProfiles(); + try { + console.log("[ActivateSelected] 开始处理代理选择"); - if (!profileData || !proxiesData) return; + const [proxiesData, profileData] = await Promise.all([ + getProxies(), + getProfiles(), + ]); - const current = profileData.items?.find( - (e) => e && e.uid === profileData.current, - ); - - if (!current) return; - - // init selected array - const { selected = [] } = current; - const selectedMap = Object.fromEntries( - selected.map((each) => [each.name!, each.now!]), - ); - - let hasChange = false; - - const newSelected: typeof selected = []; - const { global, groups } = proxiesData; - - [global, ...groups].forEach(({ type, name, now }) => { - if (!now || type !== "Selector") return; - if (selectedMap[name] != null && selectedMap[name] !== now) { - hasChange = true; - updateProxy(name, selectedMap[name]); + if (!profileData || !proxiesData) { + console.log("[ActivateSelected] 代理或配置数据不可用,跳过处理"); + return; } - newSelected.push({ name, now: selectedMap[name] }); - }); - if (hasChange) { - patchProfile(profileData.current!, { selected: newSelected }); - mutate("getProxies", getProxies()); + const current = profileData.items?.find( + (e) => e && e.uid === profileData.current, + ); + + if (!current) { + console.log("[ActivateSelected] 未找到当前profile配置"); + return; + } + + // 检查是否有saved的代理选择 + const { selected = [] } = current; + if (selected.length === 0) { + console.log("[ActivateSelected] 当前profile无保存的代理选择,跳过"); + return; + } + + console.log( + `[ActivateSelected] 当前profile有 ${selected.length} 个代理选择配置`, + ); + + const selectedMap = Object.fromEntries( + selected.map((each) => [each.name!, each.now!]), + ); + + let hasChange = false; + const newSelected: typeof selected = []; + const { global, groups } = proxiesData; + + // 处理所有代理组 + [global, ...groups].forEach(({ type, name, now }) => { + if (!now || type !== "Selector") { + if (selectedMap[name] != null) { + newSelected.push({ name, now: now || selectedMap[name] }); + } + return; + } + + const targetProxy = selectedMap[name]; + if (targetProxy != null && targetProxy !== now) { + console.log( + `[ActivateSelected] 需要切换代理组 ${name}: ${now} -> ${targetProxy}`, + ); + hasChange = true; + updateProxy(name, targetProxy); + } + + newSelected.push({ name, now: targetProxy || now }); + }); + + if (!hasChange) { + console.log("[ActivateSelected] 所有代理选择已经是目标状态,无需更新"); + return; + } + + console.log(`[ActivateSelected] 完成代理切换,保存新的选择配置`); + + try { + await patchProfile(profileData.current!, { selected: newSelected }); + console.log("[ActivateSelected] 代理选择配置保存成功"); + + setTimeout(() => { + mutate("getProxies", getProxies()); + }, 100); + } catch (error: any) { + console.error( + "[ActivateSelected] 保存代理选择配置失败:", + error.message, + ); + } + } catch (error: any) { + console.error("[ActivateSelected] 处理代理选择失败:", error.message); } }; diff --git a/clash-verge-rev/src/pages/_layout.tsx b/clash-verge-rev/src/pages/_layout.tsx index eca75fa188..d578f23559 100644 --- a/clash-verge-rev/src/pages/_layout.tsx +++ b/clash-verge-rev/src/pages/_layout.tsx @@ -169,7 +169,13 @@ const Layout = () => { const handleNotice = useCallback( (payload: [string, string]) => { const [status, msg] = payload; - handleNoticeMessage(status, msg, t, navigate); + setTimeout(() => { + try { + handleNoticeMessage(status, msg, t, navigate); + } catch (error) { + console.error("[Layout] 处理通知消息失败:", error); + } + }, 0); }, [t, navigate], ); @@ -220,12 +226,35 @@ const Layout = () => { const cleanupWindow = setupWindowListeners(); return () => { - listeners.forEach((listener) => { - if (typeof listener.then === "function") { - listener.then((unlisten) => unlisten()); - } - }); - cleanupWindow.then((cleanup) => cleanup()); + setTimeout(() => { + listeners.forEach((listener) => { + if (typeof listener.then === "function") { + listener + .then((unlisten) => { + try { + unlisten(); + } catch (error) { + console.error("[Layout] 清理事件监听器失败:", error); + } + }) + .catch((error) => { + console.error("[Layout] 获取unlisten函数失败:", error); + }); + } + }); + + cleanupWindow + .then((cleanup) => { + try { + cleanup(); + } catch (error) { + console.error("[Layout] 清理窗口监听器失败:", error); + } + }) + .catch((error) => { + console.error("[Layout] 获取cleanup函数失败:", error); + }); + }, 0); }; }, [handleNotice]); diff --git a/clash-verge-rev/src/pages/profiles.tsx b/clash-verge-rev/src/pages/profiles.tsx index f5a8501e8e..6bbcd7d515 100644 --- a/clash-verge-rev/src/pages/profiles.tsx +++ b/clash-verge-rev/src/pages/profiles.tsx @@ -190,27 +190,53 @@ const ProfilePage = () => { } }; - const activateProfile = async (profile: string, notifySuccess: boolean) => { - // 避免大多数情况下loading态闪烁 - const reset = setTimeout(() => { - setActivatings((prev) => [...prev, profile]); - }, 100); - - try { - const success = await patchProfiles({ current: profile }); - await mutateLogs(); - closeAllConnections(); - await activateSelected(); - if (notifySuccess && success) { - showNotice("success", t("Profile Switched"), 1000); + const activateProfile = useLockFn( + async (profile: string, notifySuccess: boolean) => { + if (profiles.current === profile && !notifySuccess) { + console.log( + `[Profile] 目标profile ${profile} 已经是当前配置,跳过切换`, + ); + return; } - } catch (err: any) { - showNotice("error", err?.message || err.toString(), 4000); - } finally { - clearTimeout(reset); - setActivatings([]); - } - }; + + // 避免大多数情况下loading态闪烁 + const reset = setTimeout(() => { + setActivatings((prev) => [...prev, profile]); + }, 100); + + try { + console.log(`[Profile] 开始切换到: ${profile}`); + + const success = await patchProfiles({ current: profile }); + await mutateLogs(); + closeAllConnections(); + + if (notifySuccess && success) { + showNotice("success", t("Profile Switched"), 1000); + } + + // 立即清除loading状态 + clearTimeout(reset); + setActivatings([]); + + console.log(`[Profile] 切换到 ${profile} 完成,开始后台处理`); + + setTimeout(async () => { + try { + await activateSelected(); + console.log(`[Profile] 后台处理完成`); + } catch (err: any) { + console.warn("Failed to activate selected proxies:", err); + } + }, 50); + } catch (err: any) { + console.error(`[Profile] 切换失败:`, err); + showNotice("error", err?.message || err.toString(), 4000); + clearTimeout(reset); + setActivatings([]); + } + }, + ); const onSelect = useLockFn(async (current: string, force: boolean) => { if (!force && current === profiles.current) return; await activateProfile(current, true); @@ -300,31 +326,45 @@ const ProfilePage = () => { // 监听后端配置变更 useEffect(() => { let unlistenPromise: Promise<() => void> | undefined; - let timeoutId: ReturnType | undefined; + let lastProfileId: string | null = null; + let lastUpdateTime = 0; + const debounceDelay = 200; const setupListener = async () => { unlistenPromise = listen("profile-changed", (event) => { - console.log("Profile changed event received:", event.payload); - if (timeoutId) { - clearTimeout(timeoutId); + const newProfileId = event.payload; + const now = Date.now(); + + console.log(`[Profile] 收到配置变更事件: ${newProfileId}`); + + if ( + lastProfileId === newProfileId && + now - lastUpdateTime < debounceDelay + ) { + console.log(`[Profile] 重复事件被防抖,跳过`); + return; } - timeoutId = setTimeout(() => { - mutateProfiles(); - timeoutId = undefined; - }, 300); + lastProfileId = newProfileId; + lastUpdateTime = now; + + console.log(`[Profile] 执行配置数据刷新`); + + // 使用异步调度避免阻塞事件处理 + setTimeout(() => { + mutateProfiles().catch((error) => { + console.error("[Profile] 配置数据刷新失败:", error); + }); + }, 0); }); }; setupListener(); return () => { - if (timeoutId) { - clearTimeout(timeoutId); - } - unlistenPromise?.then((unlisten) => unlisten()); + unlistenPromise?.then((unlisten) => unlisten()).catch(console.error); }; - }, [mutateProfiles, t]); + }, [mutateProfiles]); return ( { + let profileUnlisten: Promise<() => void> | undefined; + let lastProfileId: string | null = null; + let lastUpdateTime = 0; + const refreshThrottle = 500; + + const setupEventListeners = async () => { + try { + // 监听profile切换事件 + profileUnlisten = listen("profile-changed", (event) => { + const newProfileId = event.payload; + const now = Date.now(); + + console.log(`[AppDataProvider] Profile切换事件: ${newProfileId}`); + + if ( + lastProfileId === newProfileId && + now - lastUpdateTime < refreshThrottle + ) { + console.log("[AppDataProvider] 重复事件被防抖,跳过"); + return; + } + + lastProfileId = newProfileId; + lastUpdateTime = now; + + setTimeout(async () => { + try { + console.log("[AppDataProvider] 强制刷新代理缓存"); + + const refreshPromise = Promise.race([ + forceRefreshProxies(), + new Promise((_, reject) => + setTimeout( + () => reject(new Error("forceRefreshProxies timeout")), + 8000, + ), + ), + ]); + + await refreshPromise; + + console.log("[AppDataProvider] 刷新前端代理数据"); + await refreshProxy(); + + console.log("[AppDataProvider] Profile切换的代理数据刷新完成"); + } catch (error) { + console.error("[AppDataProvider] 强制刷新代理缓存失败:", error); + + refreshProxy().catch((e) => + console.warn("[AppDataProvider] 普通刷新也失败:", e), + ); + } + }, 0); + }); + + // 监听Clash配置刷新事件(enhance操作等) + const handleRefreshClash = () => { + const now = Date.now(); + console.log("[AppDataProvider] Clash配置刷新事件"); + + if (now - lastUpdateTime > refreshThrottle) { + lastUpdateTime = now; + + setTimeout(async () => { + try { + console.log("[AppDataProvider] Clash刷新 - 强制刷新代理缓存"); + + // 添加超时保护 + const refreshPromise = Promise.race([ + forceRefreshProxies(), + new Promise((_, reject) => + setTimeout( + () => reject(new Error("forceRefreshProxies timeout")), + 8000, + ), + ), + ]); + + await refreshPromise; + await refreshProxy(); + } catch (error) { + console.error( + "[AppDataProvider] Clash刷新时强制刷新代理缓存失败:", + error, + ); + refreshProxy().catch((e) => + console.warn("[AppDataProvider] Clash刷新普通刷新也失败:", e), + ); + } + }, 0); + } + }; + + window.addEventListener( + "verge://refresh-clash-config", + handleRefreshClash, + ); + + return () => { + window.removeEventListener( + "verge://refresh-clash-config", + handleRefreshClash, + ); + }; + } catch (error) { + console.error("[AppDataProvider] 事件监听器设置失败:", error); + return () => {}; + } + }; + + const cleanupPromise = setupEventListeners(); + + return () => { + profileUnlisten?.then((unlisten) => unlisten()).catch(console.error); + cleanupPromise.then((cleanup) => cleanup()); + }; + }, [refreshProxy]); + const { data: clashConfig, mutate: refreshClashConfig } = useSWR( "getClashConfig", getClashConfig, diff --git a/clash-verge-rev/src/services/cmds.ts b/clash-verge-rev/src/services/cmds.ts index c847c464c3..5f2cbc8774 100644 --- a/clash-verge-rev/src/services/cmds.ts +++ b/clash-verge-rev/src/services/cmds.ts @@ -220,6 +220,12 @@ export async function cmdGetProxyDelay( } } +/// 用于profile切换等场景 +export async function forceRefreshProxies() { + console.log("[API] 强制刷新代理缓存"); + return invoke("force_refresh_proxies"); +} + export async function cmdTestDelay(url: string) { return invoke("test_delay", { url }); } diff --git a/dns-over-https/.github/workflows/go.yml b/dns-over-https/.github/workflows/go.yml index 7efec415ed..18cc456b0a 100644 --- a/dns-over-https/.github/workflows/go.yml +++ b/dns-over-https/.github/workflows/go.yml @@ -10,7 +10,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.24.3 + go-version: 1.24.4 id: go - name: Check out repository diff --git a/dns-over-https/go.mod b/dns-over-https/go.mod index de2f0597c7..e08c198921 100644 --- a/dns-over-https/go.mod +++ b/dns-over-https/go.mod @@ -7,14 +7,14 @@ require ( github.com/gorilla/handlers v1.5.2 github.com/infobloxopen/go-trees v0.0.0-20221216143356-66ceba885ebc github.com/miekg/dns v1.1.66 - golang.org/x/net v0.40.0 + golang.org/x/net v0.41.0 ) require ( github.com/felixge/httpsnoop v1.0.4 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/sync v0.14.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/sync v0.15.0 // indirect golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.25.0 // indirect - golang.org/x/tools v0.33.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/tools v0.34.0 // indirect ) diff --git a/dns-over-https/go.sum b/dns-over-https/go.sum index 2eb7955e88..1a52528cad 100644 --- a/dns-over-https/go.sum +++ b/dns-over-https/go.sum @@ -12,17 +12,17 @@ github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/filebrowser/.github/CODEOWNERS b/filebrowser/.github/CODEOWNERS index c6937227bb..827fbfd1cf 100644 --- a/filebrowser/.github/CODEOWNERS +++ b/filebrowser/.github/CODEOWNERS @@ -2,4 +2,4 @@ # Unless a later match takes precedence, @o1egl will be requested for # review when someone opens a pull request. -* @o1egl \ No newline at end of file +* @o1egl @hacdias diff --git a/filebrowser/.github/PULL_REQUEST_TEMPLATE.md b/filebrowser/.github/PULL_REQUEST_TEMPLATE.md index 873df26930..a8efcb42aa 100644 --- a/filebrowser/.github/PULL_REQUEST_TEMPLATE.md +++ b/filebrowser/.github/PULL_REQUEST_TEMPLATE.md @@ -11,5 +11,6 @@ Before submitting your PR, please indicate which issues are either fixed or closed by this PR. See [GitHub Help: Closing issues using keywords](https://help.github.com/articles/closing-issues-via-commit-messages/). - [ ] I am aware the project is currently in maintenance-only mode. See [README](https://github.com/filebrowser/community/blob/master/README.md) +- [ ] I am aware that translations MUST be made through [Transifex](https://app.transifex.com/file-browser/file-browser/) and that this PR is NOT a translation update - [ ] I am making a PR against the `master` branch. - [ ] I am sure File Browser can be successfully built. See [builds](https://github.com/filebrowser/community/blob/master/builds.md) and [development](https://github.com/filebrowser/community/blob/master/development.md). diff --git a/filebrowser/.goreleaser.yml b/filebrowser/.goreleaser.yml index 0e0a5d65be..9b6dc5863d 100644 --- a/filebrowser/.goreleaser.yml +++ b/filebrowser/.goreleaser.yml @@ -188,15 +188,25 @@ docker_manifests: image_templates: - "filebrowser/filebrowser:v{{ .Major }}-amd64-s6" - "filebrowser/filebrowser:v{{ .Major }}-arm64-s6" -brews: + +homebrew_casks: - name: filebrowser repository: owner: filebrowser name: homebrew-tap - directory: Formula - homepage: https://filebrowser.org commit_author: name: FileBrowser Robot email: robot@filebrowser.org + homepage: https://github.com/filebrowser/filebrowser description: File Browser is a create-your-own-cloud-kind of software where you can install it on a server, direct it to a path and then access your files through a nice web interface license: "MIT" + # make the old formula conflict with the cask: + conflicts: + - formula: filebrowser + # if your app/binary isn't signed and notarized, you'll need this: + hooks: + post: + install: | + if system_command("/usr/bin/xattr", args: ["-h"]).exit_status == 0 + system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/filebrowser"] + end diff --git a/filebrowser/CHANGELOG.md b/filebrowser/CHANGELOG.md index 7ef7bda84d..260d2c4223 100644 --- a/filebrowser/CHANGELOG.md +++ b/filebrowser/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [2.32.3](https://github.com/filebrowser/filebrowser/compare/v2.32.2...v2.32.3) (2025-06-17) + +### [2.32.2](https://github.com/filebrowser/filebrowser/compare/v2.32.1...v2.32.2) (2025-06-17) + + +### Features + +* updated for project File Browser ([#5159](https://github.com/filebrowser/filebrowser/issues/5159)) ([c34c0af](https://github.com/filebrowser/filebrowser/commit/c34c0afecf3242b16ad5d5584cd90a6ad323361c)) + ### [2.32.1](https://github.com/filebrowser/filebrowser/compare/v2.32.0...v2.32.1) (2025-06-16) diff --git a/filebrowser/README.md b/filebrowser/README.md index d49de48ef7..dc1eb3e943 100644 --- a/filebrowser/README.md +++ b/filebrowser/README.md @@ -25,28 +25,28 @@ filebrowser provides a file managing interface within a specified directory and [issues]: https://github.com/filebrowser/filebrowser/issues [discussions]: https://github.com/filebrowser/filebrowser/discussions -## Demo - -URL: https://demo.filebrowser.org/ - -Credentials: `demo`/`demo` - ## Features -Please refer to our docs at [https://filebrowser.org/features](https://filebrowser.org/features) +File Browser is a **create-your-own-cloud-kind** of software where you can install it on a server, direct it to a path and then access your files through a nice web interface. You have many available features! + +| Easy Login System | Sleek Interface | User Management | +| :----------------------: | :----------------------: | :----------------------: | +| ![](./docs/assets/1.jpg) | ![](./docs/assets/2.jpg) | ![](./docs/assets/3.jpg) | + + +| File Editing | Custom Commands | Customization | +| :----------------------: | :----------------------: | :----------------------: | +| ![](./docs/assets/4.jpg) | ![](./docs/assets/5.jpg) | ![](./docs/assets/6.jpg) | + ## Install -For installation instructions please refer to our docs at [https://filebrowser.org/installation](https://filebrowser.org/installation). +For information on how to install File Browser, please check [docs/installation.md](./docs/installation.md). ## Configuration -[Authentication Method](https://filebrowser.org/configuration/authentication-method) - You can change the way the user authenticates with the filebrowser server - -[Command Runner](https://filebrowser.org/configuration/command-runner) - The command runner is a feature that enables you to execute any shell command you want before or after a certain event. - -[Custom Branding](https://filebrowser.org/configuration/custom-branding) - You can customize your File Browser installation by change its name to any other you want, by adding a global custom style sheet and by using your own logotype if you want. +For information on how to configure File Browser, please check [docs/configuration.md](./docs/configuration.md). ## Contributing -If you're interested in contributing to this project, our docs are best places to start [https://filebrowser.org/contributing](https://filebrowser.org/contributing). +For information on how to contribute to the project, including how translations are managed, please check [docs/contributing.md](./docs/contributing.md). diff --git a/filebrowser/docs/assets/1.jpg b/filebrowser/docs/assets/1.jpg new file mode 100644 index 0000000000..413e50adf7 Binary files /dev/null and b/filebrowser/docs/assets/1.jpg differ diff --git a/filebrowser/docs/assets/2.jpg b/filebrowser/docs/assets/2.jpg new file mode 100644 index 0000000000..88c791ebaa Binary files /dev/null and b/filebrowser/docs/assets/2.jpg differ diff --git a/filebrowser/docs/assets/3.jpg b/filebrowser/docs/assets/3.jpg new file mode 100644 index 0000000000..34ba26f737 Binary files /dev/null and b/filebrowser/docs/assets/3.jpg differ diff --git a/filebrowser/docs/assets/4.jpg b/filebrowser/docs/assets/4.jpg new file mode 100644 index 0000000000..542c98df7d Binary files /dev/null and b/filebrowser/docs/assets/4.jpg differ diff --git a/filebrowser/docs/assets/5.jpg b/filebrowser/docs/assets/5.jpg new file mode 100644 index 0000000000..1c2efba97d Binary files /dev/null and b/filebrowser/docs/assets/5.jpg differ diff --git a/filebrowser/docs/assets/6.jpg b/filebrowser/docs/assets/6.jpg new file mode 100644 index 0000000000..0c65a03eff Binary files /dev/null and b/filebrowser/docs/assets/6.jpg differ diff --git a/filebrowser/docs/code-of-conduct.md b/filebrowser/docs/code-of-conduct.md new file mode 100644 index 0000000000..28bdc6071a --- /dev/null +++ b/filebrowser/docs/code-of-conduct.md @@ -0,0 +1,46 @@ +# Code of Conduct + +## Contributor Covenant Code of Conduct + +### Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +### Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +### Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +### Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hacdias@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +### Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.4, available at [https://contributor-covenant.org/version/1/4](https://contributor-covenant.org/version/1/4). + diff --git a/filebrowser/docs/configuration.md b/filebrowser/docs/configuration.md new file mode 100644 index 0000000000..e38744d4ed --- /dev/null +++ b/filebrowser/docs/configuration.md @@ -0,0 +1,148 @@ +# Configuration + +Most of the configuration can be understood through our Command Line Interface documentation. Although there are some specific topics that we want to cover on this section. + +## Custom Branding + +You are able to customize your File Browser installation by changing its name to any other you want, by adding a global custom style sheet and by using your own logotype if you want. To address this, there are three configuration options that can be changed: + +* **Name:** which is the instance name that will show up on login and signup pages. This won't replace the version message in the sidebar. +* **Disable external links:** this will disable any external links (except the ones to this documentation). +* **Folder:** is the path to a directory that can contain two items: + * **custom.css**, containing the styles you want to apply to your installation. + * **img** a directory whose files can replace the [default logotypes](../frontend/public/img) in the application. + +These options can be either set via the CLI interface using the following command: + +```sh +filebrowser config set --branding.name "My Name" \ + --branding.files "/abs/path/to/my/dir" \ + --branding.disableExternal +``` +Or can be set under 'Branding directory path' in **Settings → Global Settings**. + +> [!NOTE] +> +> If using Docker then remember to bind this directory, for example as `/home/username/containers/filebrowser/branding:/branding` + +For custom icons to be recognized you need to create `img` and `img/icons` directories and place the svg in the `branding/img` directory: + +``` +- filebrowser + - branding + - img + - icons + - logo.svg + - filebrowser.db +``` + +To replace the favicon you need to place this in the `img/icons` directory but also note that some of the other PNG icon types will be required too (see the default logotypes link above) as the browser will normally use the highest resolution option available (at a minimum the 16x16 and 32x32 options). You can use the [Real Favicon Generator](https://realfavicongenerator.net/) to generate these for you from your base image. + +The icons are cached, to make the new ones appear more quickly open developer tools in your browser, then click on the Application tab, then Storage and then 'Clear Site Data'. + +## Authentication Method + +Right now, there are three possible authentication methods. Each one of them has its own capabilities and specification. If you are interested in contributing with one more authentication method, please [check the guidelines](./contributing.md). + +### JSON Auth (default) + +We call it JSON Authentication but it is just the default authentication method and the one that is provided by default if you don't make any changes. It is set by default, but if you've made changes before you can revert to using JSON auth: + +```sh +filebrowser config set --auth.method=json +``` + +This method can also be extended with **reCAPTCHA** verification during login: + +```sh +filebrowser config set --auth.method=json \ + --recaptcha.key site-key \ + --recaptcha.secret private-key +``` + +By default, we use [Google's reCAPTCHA](https://developers.google.com/recaptcha/docs/display) service. If you live in China, or want to use other provider, you can change the host with the following command: + +```sh +filebrowser config set --recaptcha.host https://recaptcha.net +``` + +Where `https://recaptcha.net` is any provider you want. + + +> [!CAUTION] +> +> Note that you **always** need to set the `--auth.method` flag when changing authentication configurations and that it will completely overwrite your current settings. [This is a known issue.](https://github.com/filebrowser/filebrowser/issues/715) + +### Proxy Header + +If you have a reverse proxy you want to use to login your users, you do it via our `proxy` authentication method. To configure this method, your proxy must send an HTTP header containing the username of the logged in user: + +```sh +filebrowser config set --auth.method=proxy --auth.header=X-My-Header +``` + +Where `X-My-Header` is the HTTP header provided by your proxy with the username. + +> [!WARNING] +> +> File Browser will blindly trust the provided header. If the proxy can be bypassed, an attacker could simply attach the header and get admin access. + +### No Authentication + +We also provide a no authentication mechanism for users that want to use File Browser privately such in a home network. By setting this authentication method, the user with **id 1** will be used as the default users. Creating more users won't have any effect. + +```sh +filebrowser config set --auth.method=noauth +``` + +## Command Runner + +The command runner is a feature that enables you to execute any shell command you want before or after a certain event. Right now, these are the events: + +* Copy +* Rename +* Upload +* Delete +* Save + +Also, during the execution of the commands set for those hooks, there will be some environment variables available to help you perform your commands: + +* `FILE` with the full absolute path to the changed file. +* `SCOPE` with the path to user's scope. +* `TRIGGER` with the name of the event. +* `USERNAME` with the user's username. +* `DESTINATION` with the absolute path to the destination. Only used for **copy** and **rename.** + +At this moment, you can edit the commands via the command line interface, using the following commands \(please check the flag `--help` to know more about them\): + +```bash +filebrowser cmds add before_copy "echo $FILE" +filebrowser cmds rm before_copy 0 +filebrowser cmds ls +``` + +Or you can use the web interface to manage them via **Settings** → **Global Settings**. + + +## Shell commands + +Within Filebrowser you can toggle the shell (`< >` icon at the top right) and this will open a shell command window at the bottom of the screen. + +**By default no commands are available as the command list is empty** + +To enable commands these need to either be done on a per-user basis (including for the Admin user). + +You can do this by adding them in Settings > User Management > (edit user) > Commands or to *apply to all new users created from that point forward* they can be set in Settings > Global Settings + +> [!NOTE] +> +> If using a proxy manager then remember to enable websockets support for the Filebrowser proxy + +> [!NOTE] +> +> If using Docker and you want to add a new command that is not in the base image then you will need to build a custom Docker image using `filebrowser/filebrowser` as a base image. For example to add 7z: +> +> ```docker +> FROM filebrowser/filebrowser +> RUN sudo apt install p7zip-full +> ``` diff --git a/filebrowser/docs/contributing.md b/filebrowser/docs/contributing.md new file mode 100644 index 0000000000..0021c9fba8 --- /dev/null +++ b/filebrowser/docs/contributing.md @@ -0,0 +1,91 @@ +# Contributing + +If you're interested in contributing to this project, this is the best place to start. Before contributing to this project, please take a bit of time to read our [Code of Conduct](./code-of-conduct.md). Also, note that this project is open-source and licensed under [Apache License 2.0](../LICENSE). + +## Project Structure + +The backend side of the application is written in [Go](https://golang.org/), while the frontend (located on a subdirectory of the same name) is written in [Vue.js](https://vuejs.org/). Due to the tight coupling required by some features, basic knowledge of both Go and Vue.js is recommended. + +* Learn Go: [https://github.com/golang/go/wiki/Learn](https://github.com/golang/go/wiki/Learn) +* Learn Vue.js: [https://vuejs.org/guide/introduction.html](https://vuejs.org/guide/introduction.html) + +We encourage you to use git to manage your fork. To clone the main repository, just run: + +```bash +git clone https://github.com/filebrowser/filebrowser +``` + +## Build + +### Frontend + +We are using [Node.js](https://nodejs.org/en/) on the frontend to manage the build process. The steps to build it are: + +```bash +# From the root of the repo, go to frontend/ +cd frontend + +# Install the dependencies +pnpm install + +# Build the frontend +pnpm run build +``` + +This will install the dependencies and build the frontend so you can then embed it into the Go app. Although, if you want to play with it, you'll get bored of building it after every change you do. So, you can run the command below to watch for changes: + +```bash +pnpm run dev +``` + +### Backend + +First of all, you need to download the required dependencies. We are using the built-in `go mod` tool for dependency management. To get the modules, run: + +```bash +go mod download +``` + +The magic of File Browser is that the static assets are bundled into the final binary. For that, we use [Go embed.FS](https://golang.org/pkg/embed/). The files from `frontend/dist` will be embedded during the build process. + +To build File Browser is just like any other Go program: + +```bash +go build +``` + +To create a development build use the "dev" tag, this way the content inside the frontend folder will not be embedded in the binary but will be reloaded at every change: + +```bash +go build -tags dev +``` + +## Translations + +Translations are managed on Transifex, which is an online website where everyone can contribute and translate strings for our project. It automatically syncs with the main language file \(in English\) and,, for you to contribute, you just need to: + +1. Go to our Transifex web page: [app.transifex.com/file-browser/file-browser](https://app.transifex.com/file-browser/file-browser/) +2. Click on **Join the project** and pick your language. We'll accept you as soon as possible. If you're language is not on the list, please request it via the interface. + +Translations are automatically pushed to GitHub via an integration. + +## Authentication Provider + +To build a new authentication provider, you need to implement the [Auther interface](https://github.com/filebrowser/filebrowser/blob/master/auth/auth.go), whose method will be called on the login page after the user has submitted their login data. + +```go +// Auther is the authentication interface. +type Auther interface { + // Auth is called to authenticate a request. + Auth(r *http.Request, s *users.Storage, root string) (*users.User, error) +} +``` + +After implementing the interface you should: + +1. Add it to [`auth` directory](https://github.com/filebrowser/filebrowser/blob/master/auth). +2. Add it to the [configuration parser](https://github.com/filebrowser/filebrowser/blob/master/cmd/config.go) for the CLI. +3. Add it to the [`authBackend.Get`](https://github.com/filebrowser/filebrowser/blob/master/storage/bolt/auth.go). + +If you need to add more flags, please update the function `addConfigFlags`. + diff --git a/filebrowser/docs/installation.md b/filebrowser/docs/installation.md new file mode 100644 index 0000000000..5475d08097 --- /dev/null +++ b/filebrowser/docs/installation.md @@ -0,0 +1,85 @@ +# Installation + +File Browser is a single binary and can be used as a standalone executable. Although, some might prefer to use it with [Docker](https://www.docker.com) or [Caddy](https://caddyserver.com), which is a fantastic web server that enables HTTPS by default. Its installation is quite straightforward independently on which system you want to use. + +## Quick Setup + +The quickest way for beginners to start using File Browser is by opening your terminal and executing the following commands: + +### Brew + +```sh +brew tap filebrowser/tap +brew install filebrowser +filebrowser -r /path/to/your/files +``` + +### Unix + +```sh +curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash +filebrowser -r /path/to/your/files +``` + +### Windows + +```sh +iwr -useb https://raw.githubusercontent.com/filebrowser/get/master/get.ps1 | iex +filebrowser -r /path/to/your/files +``` + +### Configuring + +Done! It will bootstrap a database in which all the configurations and users are stored. Now, you can see on your command line the address in which your instance is running. You just need to go to that URL and use the following credentials: + +* Username: `admin` +* Password: (printed in your console) + +Although this is the fastest way to bootstrap an instance, we recommend you to take a look at other possible options, by checking `config init --help` and `config set --help`, to make the installation as safe and customized as it can be. + +## Docker + +File Browser is available as two different Docker images, which can be found on [Docker Hub](https://hub.docker.com/r/filebrowser/filebrowser). + +### Alpine + +```sh +docker run \ + -v /path/to/srv:/srv \ + -v /path/to/filebrowser.db:/database.db \ + -v /path/to/.filebrowser.json:/.filebrowser.json \ + -u $(id -u):$(id -g) \ + -p 8080:80 \ + filebrowser/filebrowser +``` + +Where: + +- `/path/to/srv` contains the files root directory for File Browser +- `/path/to/filebrowser.db` is the `database.db` +- `/path/to/database` is the `.filebrowser.json` + +> [!Warning] +> +> To use this image correctly, you need to first initialize a File Browser database outside of the Docker image and then start the Docker image with the database mounted. Otherwise, Docker will create an empty directory at the mounting point and fail to start. + +### s6 overlay + +The `s6` image is based on LinuxServer and leverages the [s6-overlay](https://github.com/just-containers/s6-overlay) system for a standard, highly customizable image. It should be used as follows: + +```shell +docker run \ + -v /path/to/srv:/srv \ + -v /path/to/database:/database \ + -v /path/to/config:/config \ + -e PUID=$(id -u) \ + -e PGID=$(id -g) \ + -p 8080:80 \ + filebrowser/filebrowser:s6 +``` + +Where: + +- `/path/to/srv` contains the files root directory for File Browser +- `/path/to/config` contains a `settings.json` file +- `/path/to/database` contains a `filebrowser.db` file diff --git a/filebrowser/SECURITY.md b/filebrowser/docs/security.md similarity index 100% rename from filebrowser/SECURITY.md rename to filebrowser/docs/security.md diff --git a/filebrowser/frontend/src/i18n/ru.json b/filebrowser/frontend/src/i18n/ru.json index bb6e8c92f5..440e285bd4 100644 --- a/filebrowser/frontend/src/i18n/ru.json +++ b/filebrowser/frontend/src/i18n/ru.json @@ -3,14 +3,17 @@ "cancel": "Отмена", "clear": "Очистить", "close": "Закрыть", + "continue": "Продолжить", "copy": "Копировать", "copyFile": "Скопировать файл", "copyToClipboard": "Скопировать в буфер", + "copyDownloadLinkToClipboard": "Скопировать ссылку в буфер", "create": "Создать", "delete": "Удалить", "download": "Скачать", "file": "Файл", "folder": "Папка", + "fullScreen": " Развернуть на весь экран", "hideDotfiles": "Скрыть точечные файлы", "info": "Инфо", "more": "Еще", @@ -21,6 +24,7 @@ "ok": "OK", "permalink": "Получить постоянную ссылку", "previous": "Назад", + "preview": "Предпросмотр", "publish": "Опубликовать", "rename": "Переименовать", "replace": "Перезаписать", @@ -37,13 +41,17 @@ "toggleSidebar": "Боковая панель", "update": "Обновить", "upload": "Загрузить", - "openFile": "Открыть файл" + "openFile": "Открыть файл", + "discardChanges": "Отказаться" }, "download": { "downloadFile": "Скачать файл", "downloadFolder": "Загрузить папку", "downloadSelected": "Скачать выбранное" }, + "upload": { + "abortUpload": "Вы действительно, что хотите прервать операцию?" + }, "errors": { "forbidden": "У вас нет прав доступа к этому.", "internal": "Что-то пошло не так.", @@ -72,7 +80,7 @@ "click": "выбрать файл или каталог", "ctrl": { "click": "выбрать несколько файлов или каталогов", - "f": "открыть поиск", + "f": "открытые поиски", "s": "скачать файл или текущий каталог" }, "del": "удалить выбранные элементы", @@ -102,6 +110,7 @@ "deleteMessageMultiple": "Удалить эти файлы ({count})?", "deleteMessageSingle": "Удалить этот файл/каталог?", "deleteMessageShare": "Удалить этот общий файл/каталог ({path})?", + "deleteUser": "Вы действительно, хотите удалить пользователя?", "deleteTitle": "Удалить файлы", "displayName": "Отображаемое имя:", "download": "Скачать файлы", @@ -111,7 +120,7 @@ "filesSelected": "Файлов выбрано: {count}.", "lastModified": "Последнее изменение", "move": "Переместить", - "moveMessage": "Переместить в:", + "moveMessage": "Выберите новый домашний каталог для ваших файлов/папок:", "newArchetype": "Создайте новую запись на основе архетипа. Файл будет создан в каталоге.", "newDir": "Новый каталог", "newDirMessage": "Имя нового каталога.", @@ -128,8 +137,11 @@ "show": "Показать", "size": "Размер", "upload": "Загрузить", + "uploadFiles": "Загружаю {files} файлы...", "uploadMessage": "Выберите вариант для загрузки.", - "optionalPassword": "Необязательный пароль" + "optionalPassword": "Необязательный пароль", + "resolution": "Разрешение", + "discardEditorChanges": "Вы действительно желаете отменить ваши правки?" }, "search": { "images": "Изображения", @@ -158,6 +170,13 @@ "commandRunnerHelp": "Здесь вы можете установить команды, которые будут выполняться в указанных событиях. Вы должны указать по одной команде в каждой строке. Переменные среды {0} и {1} будут доступны, будучи {0} относительно {1}. Дополнительные сведения об этой функции и доступных переменных среды см. В {2}.", "commandsUpdated": "Команды обновлены!", "createUserDir": "Автоматическое создание домашнего каталога пользователя при добавлении нового пользователя", + "tusUploads": "Загруженные файлы", + "tusUploadsHelp": " File Browser поддерживает загрузку файлов по частям, что позволяет работать в сетях низкого качества.", + "tusUploadsChunkSize": "Указывает максимальный размер запроса (мелкие загрузки пойдут напрямую). Вы можете ввести простое целое число, обозначающее размер ввода в байтах, или строку, например 10MB, 1GB и т. д.", + "tusUploadsRetryCount": "Количество повторных попыток, которые необходимо выполнить, если фрагмент не удалось загрузить.", + "userHomeBasePath": "Путь к домашнему каталогу пользователя", + "userScopeGenerationPlaceholder": "Область действия будет сгенерирована автоматически", + "createUserHomeDirectory": "Создать домашний каталог пользователя", "customStylesheet": "Свой стиль", "defaultUserDescription": "Это настройки по умолчанию для новых пользователей.", "disableExternalLinks": "Отключить внешние ссылки (кроме документации)", @@ -196,7 +215,7 @@ "ruleExample2": "блокирует доступ к файлу с именем Caddyfile в корневой области.", "rules": "Права", "rulesHelp": "Здесь вы можете определить набор разрешающих и запрещающих правил для этого конкретного пользователь. Блокированные файлы не будут отображаться в списках, и не будут доступны для пользователя. Есть поддержка регулярных выражений и относительных путей.\n", - "scope": "Корень", + "scope": "Область", "setDateFormat": "Установить точный формат даты", "settingsUpdated": "Настройки применены!", "shareDuration": "Время расшаренной ссылки", @@ -204,6 +223,7 @@ "shareDeleted": "Расшаренная ссылка удалена!", "singleClick": "Открытие файлов и каталогов одним кликом", "themes": { + "default": " Системные настройки по умолчанию", "dark": "Темная", "light": "Светлая", "title": "Тема" diff --git a/filebrowser/frontend/src/views/settings/Global.vue b/filebrowser/frontend/src/views/settings/Global.vue index 5bbaec7f4d..266ad68be4 100644 --- a/filebrowser/frontend/src/views/settings/Global.vue +++ b/filebrowser/frontend/src/views/settings/Global.vue @@ -53,7 +53,7 @@ {{ t("settings.documentation") }} @@ -192,7 +192,7 @@ {{ t("settings.documentation") }} diff --git a/filebrowser/transifex.yml b/filebrowser/transifex.yml index a3fc88f1e1..9e0be9e267 100644 --- a/filebrowser/transifex.yml +++ b/filebrowser/transifex.yml @@ -6,7 +6,7 @@ filters: translation_files_expression: 'frontend/src/i18n/.json' settings: - language_mapping: + language_mapping: sv_SE: sv-se cz-CS: cz_cs pt_BR: pt-br diff --git a/lede/package/libs/libnftnl/Makefile b/lede/package/libs/libnftnl/Makefile index 50ad11a2cf..054c2a82ae 100644 --- a/lede/package/libs/libnftnl/Makefile +++ b/lede/package/libs/libnftnl/Makefile @@ -9,19 +9,21 @@ include $(TOPDIR)/rules.mk PKG_NAME:=libnftnl PKG_CPE_ID:=cpe:/a:netfilter:libnftnl -PKG_VERSION:=1.2.4 -PKG_RELEASE:=$(AUTORELEASE) +PKG_VERSION:=1.2.8 +PKG_RELEASE:=2 -PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.bz2 +PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.xz PKG_SOURCE_URL:=https://netfilter.org/projects/$(PKG_NAME)/files -PKG_HASH:=c0fe233be4cdfd703e7d5977ef8eb63fcbf1d0052b6044e1b23d47ca3562477f +PKG_HASH:=37fea5d6b5c9b08de7920d298de3cdc942e7ae64b1a3e8b880b2d390ae67ad95 PKG_MAINTAINER:=Steven Barth PKG_LICENSE:=GPL-2.0-or-later PKG_LICENSE_FILES:=COPYING +PKG_FIXUP:=autoreconf PKG_INSTALL:=1 PKG_BUILD_PARALLEL:=1 +PKG_BUILD_FLAGS:=lto include $(INCLUDE_DIR)/package.mk @@ -41,8 +43,7 @@ define Package/libnftnl/description programming interface (API) to the in-kernel nf_tables subsystem. endef -TARGET_CFLAGS += $(FPIC) -flto -TARGET_LDFLAGS += -flto +TARGET_CFLAGS += $(FPIC) CONFIGURE_ARGS += \ --enable-static \ diff --git a/lede/package/libs/libnftnl/patches/001-libnftnl-add-fullcone-expression-support.patch b/lede/package/libs/libnftnl/patches/001-libnftnl-add-fullcone-expression-support.patch new file mode 100644 index 0000000000..3bdfdfe824 --- /dev/null +++ b/lede/package/libs/libnftnl/patches/001-libnftnl-add-fullcone-expression-support.patch @@ -0,0 +1,261 @@ +From 6c39f04febd7cfdbd474233379416babcd0fc341 Mon Sep 17 00:00:00 2001 +From: Syrone Wong +Date: Fri, 8 Apr 2022 23:52:11 +0800 +Subject: [PATCH] libnftnl: add fullcone expression support + +Signed-off-by: Syrone Wong +--- + include/libnftnl/expr.h | 6 + + include/linux/netfilter/nf_tables.h | 16 +++ + src/Makefile.am | 1 + + src/expr/fullcone.c | 167 ++++++++++++++++++++++++++++ + src/expr_ops.c | 2 + + 5 files changed, 192 insertions(+) + create mode 100644 src/expr/fullcone.c + +--- a/include/libnftnl/expr.h ++++ b/include/libnftnl/expr.h +@@ -272,6 +272,13 @@ enum { + }; + + enum { ++ NFTNL_EXPR_FULLCONE_FLAGS = NFTNL_EXPR_BASE, ++ NFTNL_EXPR_FULLCONE_REG_PROTO_MIN, ++ NFTNL_EXPR_FULLCONE_REG_PROTO_MAX, ++ __NFTNL_EXPR_FULLCONE_MAX ++}; ++ ++enum { + NFTNL_EXPR_REDIR_REG_PROTO_MIN = NFTNL_EXPR_BASE, + NFTNL_EXPR_REDIR_REG_PROTO_MAX, + NFTNL_EXPR_REDIR_FLAGS, +--- a/include/linux/netfilter/nf_tables.h ++++ b/include/linux/netfilter/nf_tables.h +@@ -1464,6 +1464,22 @@ enum nft_masq_attributes { + #define NFTA_MASQ_MAX (__NFTA_MASQ_MAX - 1) + + /** ++ * enum nft_fullcone_attributes - nf_tables fullcone expression attributes ++ * ++ * @NFTA_FULLCONE_FLAGS: NAT flags (see NF_NAT_RANGE_* in linux/netfilter/nf_nat.h) (NLA_U32) ++ * @NFTA_FULLCONE_REG_PROTO_MIN: source register of proto range start (NLA_U32: nft_registers) ++ * @NFTA_FULLCONE_REG_PROTO_MAX: source register of proto range end (NLA_U32: nft_registers) ++ */ ++enum nft_fullcone_attributes { ++ NFTA_FULLCONE_UNSPEC, ++ NFTA_FULLCONE_FLAGS, ++ NFTA_FULLCONE_REG_PROTO_MIN, ++ NFTA_FULLCONE_REG_PROTO_MAX, ++ __NFTA_FULLCONE_MAX ++}; ++#define NFTA_FULLCONE_MAX (__NFTA_FULLCONE_MAX - 1) ++ ++/** + * enum nft_redir_attributes - nf_tables redirect expression netlink attributes + * + * @NFTA_REDIR_REG_PROTO_MIN: source register of proto range start (NLA_U32: nft_registers) +--- a/src/Makefile.am ++++ b/src/Makefile.am +@@ -55,6 +55,7 @@ libnftnl_la_SOURCES = utils.c \ + expr/target.c \ + expr/tunnel.c \ + expr/masq.c \ ++ expr/fullcone.c \ + expr/redir.c \ + expr/hash.c \ + expr/socket.c \ +--- /dev/null ++++ b/src/expr/fullcone.c +@@ -0,0 +1,174 @@ ++/* ++ * (C) 2022-2025 wongsyrone ++ * ++ * 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 2 of the License, or ++ * (at your option) any later version. ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++ ++#include ++ ++#include "internal.h" ++#include ++#include ++#include ++ ++struct nftnl_expr_fullcone { ++ uint32_t flags; ++ enum nft_registers sreg_proto_min; ++ enum nft_registers sreg_proto_max; ++}; ++ ++static int ++nftnl_expr_fullcone_set(struct nftnl_expr *e, uint16_t type, ++ const void *data, uint32_t data_len) ++{ ++ struct nftnl_expr_fullcone *fullcone = nftnl_expr_data(e); ++ ++ switch (type) { ++ case NFTNL_EXPR_FULLCONE_FLAGS: ++ memcpy(&fullcone->flags, data, data_len); ++ break; ++ case NFTNL_EXPR_FULLCONE_REG_PROTO_MIN: ++ memcpy(&fullcone->sreg_proto_min, data, data_len); ++ break; ++ case NFTNL_EXPR_FULLCONE_REG_PROTO_MAX: ++ memcpy(&fullcone->sreg_proto_max, data, data_len); ++ break; ++ default: ++ return -1; ++ } ++ return 0; ++} ++ ++static const void * ++nftnl_expr_fullcone_get(const struct nftnl_expr *e, uint16_t type, ++ uint32_t *data_len) ++{ ++ struct nftnl_expr_fullcone *fullcone = nftnl_expr_data(e); ++ ++ switch (type) { ++ case NFTNL_EXPR_FULLCONE_FLAGS: ++ *data_len = sizeof(fullcone->flags); ++ return &fullcone->flags; ++ case NFTNL_EXPR_FULLCONE_REG_PROTO_MIN: ++ *data_len = sizeof(fullcone->sreg_proto_min); ++ return &fullcone->sreg_proto_min; ++ case NFTNL_EXPR_FULLCONE_REG_PROTO_MAX: ++ *data_len = sizeof(fullcone->sreg_proto_max); ++ return &fullcone->sreg_proto_max; ++ } ++ return NULL; ++} ++ ++static int nftnl_expr_fullcone_cb(const struct nlattr *attr, void *data) ++{ ++ const struct nlattr **tb = data; ++ int type = mnl_attr_get_type(attr); ++ ++ if (mnl_attr_type_valid(attr, NFTA_FULLCONE_MAX) < 0) ++ return MNL_CB_OK; ++ ++ switch (type) { ++ case NFTA_FULLCONE_REG_PROTO_MIN: ++ case NFTA_FULLCONE_REG_PROTO_MAX: ++ case NFTA_FULLCONE_FLAGS: ++ if (mnl_attr_validate(attr, MNL_TYPE_U32) < 0) ++ abi_breakage(); ++ break; ++ } ++ ++ tb[type] = attr; ++ return MNL_CB_OK; ++} ++ ++static void ++nftnl_expr_fullcone_build(struct nlmsghdr *nlh, const struct nftnl_expr *e) ++{ ++ struct nftnl_expr_fullcone *fullcone = nftnl_expr_data(e); ++ ++ if (e->flags & (1 << NFTNL_EXPR_FULLCONE_FLAGS)) ++ mnl_attr_put_u32(nlh, NFTA_FULLCONE_FLAGS, htobe32(fullcone->flags)); ++ if (e->flags & (1 << NFTNL_EXPR_FULLCONE_REG_PROTO_MIN)) ++ mnl_attr_put_u32(nlh, NFTA_FULLCONE_REG_PROTO_MIN, ++ htobe32(fullcone->sreg_proto_min)); ++ if (e->flags & (1 << NFTNL_EXPR_FULLCONE_REG_PROTO_MAX)) ++ mnl_attr_put_u32(nlh, NFTA_FULLCONE_REG_PROTO_MAX, ++ htobe32(fullcone->sreg_proto_max)); ++} ++ ++static int ++nftnl_expr_fullcone_parse(struct nftnl_expr *e, struct nlattr *attr) ++{ ++ struct nftnl_expr_fullcone *fullcone = nftnl_expr_data(e); ++ struct nlattr *tb[NFTA_FULLCONE_MAX+1] = {}; ++ ++ if (mnl_attr_parse_nested(attr, nftnl_expr_fullcone_cb, tb) < 0) ++ return -1; ++ ++ if (tb[NFTA_FULLCONE_FLAGS]) { ++ fullcone->flags = be32toh(mnl_attr_get_u32(tb[NFTA_FULLCONE_FLAGS])); ++ e->flags |= (1 << NFTNL_EXPR_FULLCONE_FLAGS); ++ } ++ if (tb[NFTA_FULLCONE_REG_PROTO_MIN]) { ++ fullcone->sreg_proto_min = ++ be32toh(mnl_attr_get_u32(tb[NFTA_FULLCONE_REG_PROTO_MIN])); ++ e->flags |= (1 << NFTNL_EXPR_FULLCONE_REG_PROTO_MIN); ++ } ++ if (tb[NFTA_FULLCONE_REG_PROTO_MAX]) { ++ fullcone->sreg_proto_max = ++ be32toh(mnl_attr_get_u32(tb[NFTA_FULLCONE_REG_PROTO_MAX])); ++ e->flags |= (1 << NFTNL_EXPR_FULLCONE_REG_PROTO_MAX); ++ } ++ ++ return 0; ++} ++ ++static int nftnl_expr_fullcone_snprintf(char *buf, size_t remain, ++ uint32_t flags, const struct nftnl_expr *e) ++{ ++ struct nftnl_expr_fullcone *fullcone = nftnl_expr_data(e); ++ int offset = 0, ret = 0; ++ ++ if (e->flags & (1 << NFTNL_EXPR_FULLCONE_REG_PROTO_MIN)) { ++ ret = snprintf(buf + offset, remain, "proto_min reg %u ", ++ fullcone->sreg_proto_min); ++ SNPRINTF_BUFFER_SIZE(ret, remain, offset); ++ } ++ if (e->flags & (1 << NFTNL_EXPR_FULLCONE_REG_PROTO_MAX)) { ++ ret = snprintf(buf + offset, remain, "proto_max reg %u ", ++ fullcone->sreg_proto_max); ++ SNPRINTF_BUFFER_SIZE(ret, remain, offset); ++ } ++ if (e->flags & (1 << NFTNL_EXPR_FULLCONE_FLAGS)) { ++ ret = snprintf(buf + offset, remain, "flags 0x%x ", fullcone->flags); ++ SNPRINTF_BUFFER_SIZE(ret, remain, offset); ++ } ++ ++ return offset; ++} ++ ++static struct attr_policy fullcone_attr_policy[__NFTNL_EXPR_FULLCONE_MAX] = { ++ [NFTNL_EXPR_FULLCONE_FLAGS] = { .maxlen = sizeof(uint32_t) }, ++ [NFTNL_EXPR_FULLCONE_REG_PROTO_MIN] = { .maxlen = sizeof(uint32_t) }, ++ [NFTNL_EXPR_FULLCONE_REG_PROTO_MAX] = { .maxlen = sizeof(uint32_t) }, ++}; ++ ++struct expr_ops expr_ops_fullcone = { ++ .name = "fullcone", ++ .alloc_len = sizeof(struct nftnl_expr_fullcone), ++ .nftnl_max_attr = __NFTNL_EXPR_FULLCONE_MAX - 1, ++ .attr_policy = fullcone_attr_policy, ++ .set = nftnl_expr_fullcone_set, ++ .get = nftnl_expr_fullcone_get, ++ .parse = nftnl_expr_fullcone_parse, ++ .build = nftnl_expr_fullcone_build, ++ .output = nftnl_expr_fullcone_snprintf, ++}; +--- a/src/expr_ops.c ++++ b/src/expr_ops.c +@@ -20,6 +20,7 @@ extern struct expr_ops expr_ops_limit; + extern struct expr_ops expr_ops_log; + extern struct expr_ops expr_ops_lookup; + extern struct expr_ops expr_ops_masq; ++extern struct expr_ops expr_ops_fullcone; + extern struct expr_ops expr_ops_match; + extern struct expr_ops expr_ops_meta; + extern struct expr_ops expr_ops_ng; +@@ -65,6 +66,7 @@ static struct expr_ops *expr_ops[] = { + &expr_ops_log, + &expr_ops_lookup, + &expr_ops_masq, ++ &expr_ops_fullcone, + &expr_ops_match, + &expr_ops_meta, + &expr_ops_ng, diff --git a/lede/package/network/config/firewall4/patches/001-firewall4-add-support-for-fullcone-nat.patch b/lede/package/network/config/firewall4/patches/001-firewall4-add-support-for-fullcone-nat.patch new file mode 100644 index 0000000000..9f34b1eefd --- /dev/null +++ b/lede/package/network/config/firewall4/patches/001-firewall4-add-support-for-fullcone-nat.patch @@ -0,0 +1,216 @@ +From aa3b56e289fba7425e649a608c333622ffd9c367 Mon Sep 17 00:00:00 2001 +From: Syrone Wong +Date: Sat, 9 Apr 2022 13:24:19 +0800 +Subject: [PATCH] firewall4: add fullcone support + +fullcone is drop-in replacement of masq for non-udp traffic + +add runtime fullcone rule check, disable it globally if fullcone expr is +invalid + +defaults.fullcone and defaults.fullcone6 are switches for IPv4 and IPv6 +respectively, most IPv6 traffic do NOT need this FullCone NAT functionality. + +Renew: ZiMing Mo +--- + root/etc/config/firewall | 2 ++ + root/usr/share/firewall4/templates/ruleset.uc | 16 ++++++++++++++-- + .../firewall4/templates/zone-fullcone.uc | 4 ++++ + root/usr/share/ucode/fw4.uc | 69 ++++++++++++++++++- + 4 files changed, 89 insertions(+), 4 deletions(-) + create mode 100644 root/usr/share/firewall4/templates/zone-fullcone.uc + +--- a/root/etc/config/firewall ++++ b/root/etc/config/firewall +@@ -5,6 +5,10 @@ config defaults + option forward REJECT + # Uncomment this line to disable ipv6 rules + # option disable_ipv6 1 ++ option flow_offloading 1 ++ option flow_offloading_hw 1 ++ option fullcone 1 ++ option fullcone6 0 + + config zone + option name lan +--- a/root/usr/share/firewall4/templates/ruleset.uc ++++ b/root/usr/share/firewall4/templates/ruleset.uc +@@ -327,6 +327,12 @@ table inet fw4 { + {% for (let redirect in fw4.redirects(`dstnat_${zone.name}`)): %} + {%+ include("redirect.uc", { fw4, zone, redirect }) %} + {% endfor %} ++{% if (zone.masq && fw4.default_option("fullcone")): %} ++ {%+ include("zone-fullcone.uc", { fw4, zone, family: 4, direction: "dstnat" }) %} ++{% endif %} ++{% if (zone.masq6 && fw4.default_option("fullcone6")): %} ++ {%+ include("zone-fullcone.uc", { fw4, zone, family: 6, direction: "dstnat" }) %} ++{% endif %} + {% fw4.includes('chain-append', `dstnat_${zone.name}`) %} + } + +@@ -337,20 +343,26 @@ table inet fw4 { + {% for (let redirect in fw4.redirects(`srcnat_${zone.name}`)): %} + {%+ include("redirect.uc", { fw4, zone, redirect }) %} + {% endfor %} +-{% if (zone.masq): %} ++{% if (zone.masq && !fw4.default_option("fullcone")): %} + {% for (let saddrs in zone.masq4_src_subnets): %} + {% for (let daddrs in zone.masq4_dest_subnets): %} + {%+ include("zone-masq.uc", { fw4, zone, family: 4, saddrs, daddrs }) %} + {% endfor %} + {% endfor %} + {% endif %} +-{% if (zone.masq6): %} ++{% if (zone.masq6 && !fw4.default_option("fullcone6")): %} + {% for (let saddrs in zone.masq6_src_subnets): %} + {% for (let daddrs in zone.masq6_dest_subnets): %} + {%+ include("zone-masq.uc", { fw4, zone, family: 6, saddrs, daddrs }) %} + {% endfor %} + {% endfor %} + {% endif %} ++{% if (zone.masq && fw4.default_option("fullcone")): %} ++ {%+ include("zone-fullcone.uc", { fw4, zone, family: 4, direction: "srcnat" }) %} ++{% endif %} ++{% if (zone.masq6 && fw4.default_option("fullcone6")): %} ++ {%+ include("zone-fullcone.uc", { fw4, zone, family: 6, direction: "srcnat" }) %} ++{% endif %} + {% fw4.includes('chain-append', `srcnat_${zone.name}`) %} + } + +--- /dev/null ++++ b/root/usr/share/firewall4/templates/zone-fullcone.uc +@@ -0,0 +1,4 @@ ++{# /usr/share/firewall4/templates/zone-fullcone.uc #} ++ meta nfproto {{ fw4.nfproto(family) }} fullcone comment "!fw4: Handle {{ ++ zone.name ++}} {{ fw4.nfproto(family, true) }} fullcone NAT {{ direction }} traffic" +--- a/root/usr/share/ucode/fw4.uc ++++ b/root/usr/share/ucode/fw4.uc +@@ -1,3 +1,5 @@ ++// /usr/share/ucode/fw4.uc ++ + const fs = require("fs"); + const uci = require("uci"); + const ubus = require("ubus"); +@@ -489,6 +491,25 @@ function nft_try_hw_offload(devices) { + return (rc == 0); + } + ++function nft_try_fullcone() { ++ let nft_test = ++ 'add table inet fw4-fullcone-test; ' + ++ 'add chain inet fw4-fullcone-test dstnat { ' + ++ 'type nat hook prerouting priority -100; policy accept; ' + ++ 'fullcone; ' + ++ '}; ' + ++ 'add chain inet fw4-fullcone-test srcnat { ' + ++ 'type nat hook postrouting priority -100; policy accept; ' + ++ 'fullcone; ' + ++ '}; '; ++ let cmd = sprintf("/usr/sbin/nft -c '%s' 2>/dev/null", replace(nft_test, "'", "'\\''")); ++ let ok = system(cmd) == 0; ++ if (!ok) { ++ warn("nft_try_fullcone: cmd "+ cmd + "\n"); ++ } ++ return ok; ++} ++ + + return { + read_kernel_version: function() { +@@ -855,6 +876,18 @@ return { + warn(`[!] ${msg}\n`); + }, + ++ myinfo: function(fmt, ...args) { ++ if (getenv("QUIET")) ++ return; ++ ++ let msg = sprintf(fmt, ...args); ++ ++ if (getenv("TTY")) ++ warn(`\033[32m${msg}\033[m\n`); ++ else ++ warn(`[I] ${msg}\n`); ++ }, ++ + get: function(sid, opt) { + return this.cursor.get("firewall", sid, opt); + }, +@@ -1036,6 +1069,21 @@ return { + } + }, + ++ myinfo_section: function(s, msg) { ++ if (s[".name"]) { ++ if (s.name) ++ this.myinfo("Section %s (%s) %s", this.section_id(s[".name"]), s.name, msg); ++ else ++ this.myinfo("Section %s %s", this.section_id(s[".name"]), msg); ++ } ++ else { ++ if (s.name) ++ this.myinfo("ubus %s (%s) %s", s.type || "rule", s.name, msg); ++ else ++ this.myinfo("ubus %s %s", s.type || "rule", msg); ++ } ++ }, ++ + parse_policy: function(val) { + return this.parse_enum(val, [ + "accept", +@@ -1475,6 +1523,7 @@ return { + "dnat", + "snat", + "masquerade", ++ "fullcone", + "accept", + "reject", + "drop" +@@ -1946,6 +1995,8 @@ return { + } + + let defs = this.parse_options(data, { ++ fullcone: [ "bool", "0" ], ++ fullcone6: [ "bool", "0" ], + input: [ "policy", "drop" ], + output: [ "policy", "drop" ], + forward: [ "policy", "drop" ], +@@ -1980,6 +2031,11 @@ return { + + delete defs.syn_flood; + ++ if (!nft_try_fullcone()) { ++ delete defs.fullcone; ++ warn("nft_try_fullcone failed, disable fullcone globally\n"); ++ } ++ + this.state.defaults = defs; + }, + +@@ -2205,10 +2261,23 @@ return { + zone.related_subnets = related_subnets; + zone.related_physdevs = related_physdevs; + +- if (zone.masq || zone.masq6) ++ if (zone.masq) { + zone.dflags.snat = true; ++ if (this.state.defaults.fullcone) { ++ zone.dflags.dnat = true; ++ this.myinfo_section(data, "IPv4 fullcone enabled for zone '" + zone.name + "'"); ++ } ++ } ++ ++ if (zone.masq6) { ++ zone.dflags.snat = true; ++ if (this.state.defaults.fullcone6) { ++ zone.dflags.dnat = true; ++ this.myinfo_section(data, "IPv6 fullcone enabled for zone '" + zone.name + "'"); ++ } ++ } + +- if ((zone.auto_helper && !(zone.masq || zone.masq6)) || length(zone.helper)) { ++ if ((zone.auto_helper && !(zone.masq || zone.masq6 || this.state.defaults.fullcone || this.state.defaults.fullcone6)) || length(zone.helper)) { + zone.dflags.helper = true; + + for (let helper in (length(zone.helper) ? zone.helper : this.state.helpers)) { diff --git a/lede/package/network/services/fullconenat-nft/Makefile b/lede/package/network/services/fullconenat-nft/Makefile new file mode 100644 index 0000000000..74c7784b45 --- /dev/null +++ b/lede/package/network/services/fullconenat-nft/Makefile @@ -0,0 +1,50 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright (c) 2018 Chion Tang +# Original xt_FULLCONENAT and related iptables extension author +# Copyright (c) 2019-2022 GitHub/llccd Twitter/@gNodeB +# Added IPv6 support for xt_FULLCONENAT and ip6tables extension +# Ported to recent kernel versions +# Copyright (c) 2022 Syrone Wong +# Massively rewrite the whole module, split the original code into library and nftables 'fullcone' expression module + +include $(TOPDIR)/rules.mk +include $(INCLUDE_DIR)/kernel.mk + +PKG_NAME:=fullconenat-nft +PKG_RELEASE:=2 + +PKG_SOURCE_PROTO:=git +PKG_SOURCE_URL:=https://github.com/fullcone-nat-nftables/nft-fullcone.git +PKG_SOURCE_DATE:=2023-05-17 +PKG_SOURCE_VERSION:=07d93b626ce5ea885cd16f9ab07fac3213c355d9 +PKG_MIRROR_HASH:=b89c68c68b5912f20cefed703c993498fed612ba4860fa75ef50037cb79a32f5 + +PKG_LICENSE:=GPL-2.0-only +PKG_LICENSE_FILES:=LICENSE +PKG_MAINTAINER:=Syrone Wong + +include $(INCLUDE_DIR)/package.mk + +define KernelPackage/nft-fullcone + SUBMENU:=Netfilter Extensions + DEPENDS:=+kmod-nft-nat + TITLE:=nftables fullcone expression support + FILES:= $(PKG_BUILD_DIR)/src/nft_fullcone.ko + KCONFIG:= \ + CONFIG_NF_CONNTRACK_EVENTS=y \ + CONFIG_NF_CONNTRACK_CHAIN_EVENTS=y + AUTOLOAD:=$(call AutoProbe,nft_fullcone) +endef + +define KernelPackage/nft-fullcone/Description + Kernel module adds the fullcone expression that you can use + to perform NAT in the RFC3489-compatible full cone SNAT flavour. + Currently only UDP traffic is supported for full-cone NAT. + For other protos FULLCONENAT is equivalent to MASQUERADE. +endef + +define Build/Compile + +$(KERNEL_MAKE) M="$(PKG_BUILD_DIR)/src" modules +endef + +$(eval $(call KernelPackage,nft-fullcone)) diff --git a/lede/package/network/services/fullconenat-nft/patches/010-fix-build-with-kernel-6.12.patch b/lede/package/network/services/fullconenat-nft/patches/010-fix-build-with-kernel-6.12.patch new file mode 100644 index 0000000000..b7af60455a --- /dev/null +++ b/lede/package/network/services/fullconenat-nft/patches/010-fix-build-with-kernel-6.12.patch @@ -0,0 +1,14 @@ +--- a/src/nft_ext_fullcone.c ++++ b/src/nft_ext_fullcone.c +@@ -121,7 +121,11 @@ static int exp_event_cb(unsigned int eve + } + #endif + ++#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 12, 0) ++static int nft_fullcone_validate(const struct nft_ctx *ctx, const struct nft_expr *expr) ++#else + static int nft_fullcone_validate(const struct nft_ctx *ctx, const struct nft_expr *expr, const struct nft_data **data) ++#endif + { + int err; + diff --git a/lede/package/network/utils/nftables/Makefile b/lede/package/network/utils/nftables/Makefile index 2010aaf65c..bda081ac6f 100644 --- a/lede/package/network/utils/nftables/Makefile +++ b/lede/package/network/utils/nftables/Makefile @@ -6,12 +6,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=nftables -PKG_VERSION:=1.0.6 +PKG_VERSION:=1.1.1 PKG_RELEASE:=1 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.xz PKG_SOURCE_URL:=https://netfilter.org/projects/$(PKG_NAME)/files -PKG_HASH:=2407430ddd82987670e48dc2fda9e280baa8307abec04ab18d609df3db005e4c +PKG_HASH:=6358830f3a64f31e39b0ad421d7dadcd240b72343ded48d8ef13b8faf204865a PKG_MAINTAINER:= PKG_LICENSE:=GPL-2.0 @@ -20,6 +20,8 @@ PKG_LICENSE_FILES:=COPYING PKG_FIXUP:=autoreconf PKG_INSTALL:=1 +PKG_BUILD_FLAGS:=lto + include $(INCLUDE_DIR)/package.mk DISABLE_NLS:= @@ -36,7 +38,7 @@ define Package/nftables/Default CATEGORY:=Network SUBMENU:=Firewall TITLE:=nftables userspace utility - DEPENDS:=+kmod-nft-core +libnftnl + DEPENDS:=+kmod-nft-core +libnftnl +kmod-nft-fullcone URL:=http://netfilter.org/projects/nftables/ PROVIDES:=nftables endef @@ -60,9 +62,6 @@ ifeq ($(BUILD_VARIANT),json) CONFIGURE_ARGS += --with-json endif -TARGET_CFLAGS += -flto -TARGET_LDFLAGS += -flto - define Build/InstallDev $(INSTALL_DIR) $(1)/usr/lib $(1)/usr/include $(CP) $(PKG_INSTALL_DIR)/usr/lib/*.so* $(1)/usr/lib/ diff --git a/lede/package/network/utils/nftables/patches/001-drop-useless-file.patch b/lede/package/network/utils/nftables/patches/001-drop-useless-file.patch new file mode 100644 index 0000000000..bad5600b5a --- /dev/null +++ b/lede/package/network/utils/nftables/patches/001-drop-useless-file.patch @@ -0,0 +1,18 @@ +--- a/tests/shell/run-tests.sh.rej ++++ /dev/null +@@ -1,15 +0,0 @@ +---- run-tests.sh +-+++ run-tests.sh +-@@ -565,11 +565,8 @@ feature_probe() +- fi +- +- if [ -x "$with_path.sh" ] ; then +-- echo $with_path +- NFT="$NFT_REAL" $NFT_TEST_UNSHARE_CMD "$with_path.sh" &>/dev/null +-- RET=$? +-- echo $? +-- return $RET +-+ return $? +- fi +- +- return 1 diff --git a/lede/package/network/utils/nftables/patches/002-nftables-add-fullcone-expression-support.patch b/lede/package/network/utils/nftables/patches/002-nftables-add-fullcone-expression-support.patch new file mode 100644 index 0000000000..06679839e9 --- /dev/null +++ b/lede/package/network/utils/nftables/patches/002-nftables-add-fullcone-expression-support.patch @@ -0,0 +1,209 @@ +From 58c89e8768711a959fdc6e953df3ea2254ff93c1 Mon Sep 17 00:00:00 2001 +From: Syrone Wong +Date: Sat, 9 Apr 2022 00:38:51 +0800 +Subject: [PATCH] nftables: add fullcone expression support + +Signed-off-by: Syrone Wong +--- + include/linux/netfilter/nf_tables.h | 16 ++++++++++ + include/statement.h | 1 + + src/netlink_delinearize.c | 48 +++++++++++++++++++++++++++++ + src/netlink_linearize.c | 7 +++++ + src/parser_bison.y | 28 +++++++++++++++-- + src/scanner.l | 1 + + src/statement.c | 1 + + 7 files changed, 100 insertions(+), 2 deletions(-) + +--- a/include/linux/netfilter/nf_tables.h ++++ b/include/linux/netfilter/nf_tables.h +@@ -1485,6 +1485,22 @@ enum nft_masq_attributes { + #define NFTA_MASQ_MAX (__NFTA_MASQ_MAX - 1) + + /** ++ * enum nft_fullcone_attributes - nf_tables fullcone expression attributes ++ * ++ * @NFTA_FULLCONE_FLAGS: NAT flags (see NF_NAT_RANGE_* in linux/netfilter/nf_nat.h) (NLA_U32) ++ * @NFTA_FULLCONE_REG_PROTO_MIN: source register of proto range start (NLA_U32: nft_registers) ++ * @NFTA_FULLCONE_REG_PROTO_MAX: source register of proto range end (NLA_U32: nft_registers) ++ */ ++enum nft_fullcone_attributes { ++ NFTA_FULLCONE_UNSPEC, ++ NFTA_FULLCONE_FLAGS, ++ NFTA_FULLCONE_REG_PROTO_MIN, ++ NFTA_FULLCONE_REG_PROTO_MAX, ++ __NFTA_FULLCONE_MAX ++}; ++#define NFTA_FULLCONE_MAX (__NFTA_FULLCONE_MAX - 1) ++ ++/** + * enum nft_redir_attributes - nf_tables redirect expression netlink attributes + * + * @NFTA_REDIR_REG_PROTO_MIN: source register of proto range start (NLA_U32: nft_registers) +--- a/include/statement.h ++++ b/include/statement.h +@@ -129,6 +129,7 @@ enum nft_nat_etypes { + __NFT_NAT_SNAT = NFT_NAT_SNAT, + __NFT_NAT_DNAT = NFT_NAT_DNAT, + NFT_NAT_MASQ, ++ NFT_NAT_FULLCONE, + NFT_NAT_REDIR, + }; + +--- a/src/netlink_delinearize.c ++++ b/src/netlink_delinearize.c +@@ -1467,6 +1467,53 @@ out_err: + stmt_free(stmt); + } + ++static void netlink_parse_fullcone(struct netlink_parse_ctx *ctx, ++ const struct location *loc, ++ const struct nftnl_expr *nle) ++{ ++ enum nft_registers reg1, reg2; ++ struct expr *proto; ++ struct stmt *stmt; ++ uint32_t flags = 0; ++ ++ if (nftnl_expr_is_set(nle, NFTNL_EXPR_FULLCONE_FLAGS)) ++ flags = nftnl_expr_get_u32(nle, NFTNL_EXPR_FULLCONE_FLAGS); ++ ++ stmt = nat_stmt_alloc(loc, NFT_NAT_FULLCONE); ++ stmt->nat.flags = flags; ++ ++ reg1 = netlink_parse_register(nle, NFTNL_EXPR_FULLCONE_REG_PROTO_MIN); ++ if (reg1) { ++ proto = netlink_get_register(ctx, loc, reg1); ++ if (proto == NULL) { ++ netlink_error(ctx, loc, ++ "fullcone statement has no proto expression"); ++ goto out_err; ++ } ++ expr_set_type(proto, &inet_service_type, BYTEORDER_BIG_ENDIAN); ++ stmt->nat.proto = proto; ++ } ++ ++ reg2 = netlink_parse_register(nle, NFTNL_EXPR_FULLCONE_REG_PROTO_MAX); ++ if (reg2 && reg2 != reg1) { ++ proto = netlink_get_register(ctx, loc, reg2); ++ if (proto == NULL) { ++ netlink_error(ctx, loc, ++ "fullcone statement has no proto expression"); ++ goto out_err; ++ } ++ expr_set_type(proto, &inet_service_type, BYTEORDER_BIG_ENDIAN); ++ if (stmt->nat.proto != NULL) ++ proto = range_expr_alloc(loc, stmt->nat.proto, proto); ++ stmt->nat.proto = proto; ++ } ++ ++ ctx->stmt = stmt; ++ return; ++out_err: ++ stmt_free(stmt); ++} ++ + static void netlink_parse_redir(struct netlink_parse_ctx *ctx, + const struct location *loc, + const struct nftnl_expr *nle) +@@ -1897,6 +1944,7 @@ static const struct expr_handler netlink + { .name = "tproxy", .parse = netlink_parse_tproxy }, + { .name = "notrack", .parse = netlink_parse_notrack }, + { .name = "masq", .parse = netlink_parse_masq }, ++ { .name = "fullcone", .parse = netlink_parse_fullcone }, + { .name = "redir", .parse = netlink_parse_redir }, + { .name = "dup", .parse = netlink_parse_dup }, + { .name = "queue", .parse = netlink_parse_queue }, +--- a/src/netlink_linearize.c ++++ b/src/netlink_linearize.c +@@ -1226,6 +1226,13 @@ static void netlink_gen_nat_stmt(struct + nftnl_reg_pmin = NFTNL_EXPR_MASQ_REG_PROTO_MIN; + nftnl_reg_pmax = NFTNL_EXPR_MASQ_REG_PROTO_MAX; + break; ++ case NFT_NAT_FULLCONE: ++ nle = alloc_nft_expr("fullcone"); ++ ++ nftnl_flag_attr = NFTNL_EXPR_FULLCONE_FLAGS; ++ nftnl_reg_pmin = NFTNL_EXPR_FULLCONE_REG_PROTO_MIN; ++ nftnl_reg_pmax = NFTNL_EXPR_FULLCONE_REG_PROTO_MAX; ++ break; + case NFT_NAT_REDIR: + nle = alloc_nft_expr("redir"); + +--- a/src/parser_bison.y ++++ b/src/parser_bison.y +@@ -643,6 +643,7 @@ int nft_lex(void *, void *, void *); + %token SNAT "snat" + %token DNAT "dnat" + %token MASQUERADE "masquerade" ++%token FULLCONE "fullcone" + %token REDIRECT "redirect" + %token RANDOM "random" + %token FULLY_RANDOM "fully-random" +@@ -784,8 +785,8 @@ int nft_lex(void *, void *, void *); + %type limit_burst_pkts limit_burst_bytes limit_mode limit_bytes time_unit quota_mode + %type reject_stmt reject_stmt_alloc + %destructor { stmt_free($$); } reject_stmt reject_stmt_alloc +-%type nat_stmt nat_stmt_alloc masq_stmt masq_stmt_alloc redir_stmt redir_stmt_alloc +-%destructor { stmt_free($$); } nat_stmt nat_stmt_alloc masq_stmt masq_stmt_alloc redir_stmt redir_stmt_alloc ++%type nat_stmt nat_stmt_alloc masq_stmt masq_stmt_alloc fullcone_stmt fullcone_stmt_alloc redir_stmt redir_stmt_alloc ++%destructor { stmt_free($$); } nat_stmt nat_stmt_alloc masq_stmt masq_stmt_alloc fullcone_stmt fullcone_stmt_alloc redir_stmt redir_stmt_alloc + %type nf_nat_flags nf_nat_flag offset_opt + %type tproxy_stmt + %destructor { stmt_free($$); } tproxy_stmt +@@ -3216,6 +3217,7 @@ stmt : verdict_stmt + | queue_stmt + | ct_stmt + | masq_stmt close_scope_nat ++ | fullcone_stmt close_scope_nat + | redir_stmt close_scope_nat + | dup_stmt close_scope_dup + | fwd_stmt close_scope_fwd +@@ -3999,6 +4001,28 @@ masq_stmt_args : TO COLON stmt_expr + { + $0->nat.proto = $3; + } ++ | TO COLON stmt_expr nf_nat_flags ++ { ++ $0->nat.proto = $3; ++ $0->nat.flags = $4; ++ } ++ | nf_nat_flags ++ { ++ $0->nat.flags = $1; ++ } ++ ; ++ ++fullcone_stmt : fullcone_stmt_alloc fullcone_stmt_args ++ | fullcone_stmt_alloc ++ ; ++ ++fullcone_stmt_alloc : FULLCONE { $$ = nat_stmt_alloc(&@$, NFT_NAT_FULLCONE); } ++ ; ++ ++fullcone_stmt_args : TO COLON stmt_expr ++ { ++ $0->nat.proto = $3; ++ } + | TO COLON stmt_expr nf_nat_flags + { + $0->nat.proto = $3; +--- a/src/scanner.l ++++ b/src/scanner.l +@@ -462,6 +462,7 @@ addrstring ({macaddr}|{ip4addr}|{ip6addr + "snat" { scanner_push_start_cond(yyscanner, SCANSTATE_STMT_NAT); return SNAT; } + "dnat" { scanner_push_start_cond(yyscanner, SCANSTATE_STMT_NAT); return DNAT; } + "masquerade" { scanner_push_start_cond(yyscanner, SCANSTATE_STMT_NAT); return MASQUERADE; } ++"fullcone" { scanner_push_start_cond(yyscanner, SCANSTATE_STMT_NAT); return FULLCONE; } + "redirect" { scanner_push_start_cond(yyscanner, SCANSTATE_STMT_NAT); return REDIRECT; } + "random" { return RANDOM; } + { +--- a/src/statement.c ++++ b/src/statement.c +@@ -674,6 +674,7 @@ const char *nat_etype2str(enum nft_nat_e + [NFT_NAT_SNAT] = "snat", + [NFT_NAT_DNAT] = "dnat", + [NFT_NAT_MASQ] = "masquerade", ++ [NFT_NAT_FULLCONE] = "fullcone", + [NFT_NAT_REDIR] = "redirect", + }; + diff --git a/openwrt-packages/filebrowser/Makefile b/openwrt-packages/filebrowser/Makefile index 0c0744063a..d50e225eb4 100644 --- a/openwrt-packages/filebrowser/Makefile +++ b/openwrt-packages/filebrowser/Makefile @@ -5,12 +5,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=filebrowser -PKG_VERSION:=2.32.0 +PKG_VERSION:=2.32.3 PKG_RELEASE:=1 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/filebrowser/filebrowser/tar.gz/v${PKG_VERSION}? -PKG_HASH:=61e9de6b2d396614f45be477e5bb5aad189e7bb1155a3f88800e02421bd6cc2b +PKG_HASH:=4e215896bb2facf27f86a67d432b7c4c0fb74c45f53109763a4e7e5fb1459079 PKG_LICENSE:=Apache-2.0 PKG_LICENSE_FILES:=LICENSE diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua index cc9a5dbbc5..3e2a20a30d 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua @@ -459,21 +459,6 @@ if api.is_finded("smartdns") then end return DynamicList.write(self, section, t) end - function o.validate(self, value) --禁止私有IP - if type(value) == "table" then - for _, v in ipairs(value) do - if v:match("127%.0%.0%.") or - v:match("192%.168%.") or - v:match("10%.") or - v:match("172%.1[6-9]%.") or - v:match("172%.2[0-9]%.") or - v:match("172%.3[0-1]%.") then - return nil, translatef("Private IPs are not allowed: %s", v) - end - end - end - return value - end end o = s:taboption("DNS", ListValue, "xray_dns_mode", translate("Request protocol")) @@ -573,8 +558,7 @@ o.description = translate("Notify the DNS server when the DNS query is notified, o.datatype = "ipaddr" o:depends({dns_mode = "sing-box"}) o:depends({dns_mode = "xray"}) -o:depends("smartdns_dns_mode", "sing-box") -o:depends("smartdns_dns_mode", "xray") +o:depends("dns_shunt", "smartdns") o = s:taboption("DNS", Flag, "remote_fakedns", "FakeDNS", translate("Use FakeDNS work in the shunt domain that proxy.")) o.default = "0" diff --git a/openwrt-passwall/luci-app-passwall/po/zh-cn/passwall.po b/openwrt-passwall/luci-app-passwall/po/zh-cn/passwall.po index c3085d1fa6..c9a1fb5c46 100644 --- a/openwrt-passwall/luci-app-passwall/po/zh-cn/passwall.po +++ b/openwrt-passwall/luci-app-passwall/po/zh-cn/passwall.po @@ -118,9 +118,6 @@ msgstr "国内分组名" msgid "You only need to configure domestic DNS packets in SmartDNS and set it redirect or as Dnsmasq upstream, and fill in the domestic DNS group name here." msgstr "您只需要在SmartDNS配置好国内DNS分组,并设置重定向或作为Dnsmasq上游,此处填入国内DNS分组名。" -msgid "Private IPs are not allowed: %s" -msgstr "不允许使用私有 IP 地址:%s" - msgid "Filter Mode" msgstr "过滤模式" diff --git a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/app.sh b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/app.sh index 5d00dc0ed4..3886e87776 100755 --- a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/app.sh +++ b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/app.sh @@ -1610,12 +1610,13 @@ start_dns() { else smartdns_remote_dns="tcp://1.1.1.1" fi + local subnet_ip=$(config_t_get global remote_dns_client_ip) lua $APP_PATH/helper_smartdns_add.lua -FLAG "default" -SMARTDNS_CONF "/tmp/etc/smartdns/$CONFIG.conf" \ -LOCAL_GROUP ${group_domestic:-nil} -REMOTE_GROUP "passwall_proxy" -REMOTE_PROXY_SERVER ${TCP_SOCKS_server} -USE_DEFAULT_DNS "${USE_DEFAULT_DNS:-direct}" \ -REMOTE_DNS ${smartdns_remote_dns} -DNS_MODE ${DNS_MODE:-socks} -TUN_DNS ${TUN_DNS} -REMOTE_FAKEDNS ${fakedns:-0} \ -USE_DIRECT_LIST "${USE_DIRECT_LIST}" -USE_PROXY_LIST "${USE_PROXY_LIST}" -USE_BLOCK_LIST "${USE_BLOCK_LIST}" -USE_GFW_LIST "${USE_GFW_LIST}" -CHN_LIST "${CHN_LIST}" \ -TCP_NODE ${TCP_NODE} -DEFAULT_PROXY_MODE "${TCP_PROXY_MODE}" -NO_PROXY_IPV6 ${FILTER_PROXY_IPV6:-0} -NFTFLAG ${nftflag:-0} \ - -NO_LOGIC_LOG ${NO_LOGIC_LOG:-0} + -SUBNET ${subnet_ip:-0} -NO_LOGIC_LOG ${NO_LOGIC_LOG:-0} source $APP_PATH/helper_smartdns.sh restart echolog " - 域名解析:使用SmartDNS,请确保配置正常。" return diff --git a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/helper_smartdns_add.lua b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/helper_smartdns_add.lua index 7aa21a9381..919bfeb8e0 100644 --- a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/helper_smartdns_add.lua +++ b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/helper_smartdns_add.lua @@ -23,6 +23,7 @@ local DEFAULT_PROXY_MODE = var["-DEFAULT_PROXY_MODE"] local NO_PROXY_IPV6 = var["-NO_PROXY_IPV6"] local NO_LOGIC_LOG = var["-NO_LOGIC_LOG"] local NFTFLAG = var["-NFTFLAG"] +local SUBNET = var["-SUBNET"] local uci = api.uci local sys = api.sys @@ -168,56 +169,68 @@ config_lines = { DNS_MODE == "socks" and string.format("proxy-server socks5://%s -name %s", REMOTE_PROXY_SERVER, proxy_server_name) or nil } if DNS_MODE == "socks" then - local found_private = false - for dns in string.gmatch(REMOTE_DNS, "([^|]+)") do - -- 判断是否为私有地址 - if dns:match("127%.0%.0%.") or - dns:match("192%.168%.") or - dns:match("10%.") or - dns:match("172%.1[6-9]%.") or - dns:match("172%.2[0-9]%.") or - dns:match("172%.3[0-1]%.") then - found_private = true - break - end - end - if found_private then - REMOTE_DNS = "tcp://1.1.1.1" - log(" * 错误!SmartDNS 远程 DNS 不允许使用私有地址,将默认使用:tcp://1.1.1.1") - end - string.gsub(REMOTE_DNS, '[^' .. "|" .. ']+', function(w) - local server_dns = w - local server_param = string.format("server %s -group %s -proxy %s", "%s", REMOTE_GROUP, proxy_server_name) - server_param = server_param .. " -exclude-default-group" + for w in string.gmatch(REMOTE_DNS, '[^|]+') do + local server_dns = api.trim(w) + local server_param - local isHTTPS = w:find("https://") - if isHTTPS and isHTTPS == 1 then - local http_host = nil - local url = w - local port = 443 - local s = api.split(w, ",") - if s and #s > 1 then - url = s[1] - local dns_ip = s[2] - local host_port = api.get_domain_from_url(s[1]) - if host_port and #host_port > 0 then - http_host = host_port - local s2 = api.split(host_port, ":") - if s2 and #s2 > 1 then - http_host = s2[1] - port = s2[2] - end - url = url:gsub(http_host, dns_ip) + local dnsType = string.match(server_dns, "^(.-)://") + dnsType = dnsType and string.lower(dnsType) or nil + local dnsServer = string.match(server_dns, "://(.+)") or server_dns + + if dnsType and dnsType ~= "" and dnsType ~= "udp" then + if dnsType == "tcp" then + server_param = "server-tcp " .. dnsServer + elseif dnsType == "tls" then + server_param = "server-tls " .. dnsServer + elseif dnsType == "quic" then + server_param = "server-quic " .. dnsServer + elseif dnsType == "https" or dnsType == "h3" then + local http_host = nil + local url = w + local port = 443 + local s = api.split(w, ",") + if s and #s > 1 then + url = s[1] + local dns_ip = s[2] + local host_port = api.get_domain_from_url(s[1]) + if host_port and #host_port > 0 then + http_host = host_port + local s2 = api.split(host_port, ":") + if s2 and #s2 > 1 then + http_host = s2[1] + port = s2[2] + end + url = url:gsub(http_host, dns_ip) + end end + server_dns = url + if http_host then + server_dns = server_dns .. " -http-host " .. http_host + end + server_param = (dnsType == "https" and "server-https " or "server-h3 ") .. server_dns end - server_dns = url - if http_host then - server_dns = server_dns .. " -http-host " .. http_host - end + else + server_param = "server " .. dnsServer + + end + + -- 判断是否为本地地址 + local is_local = w:match("127%.0%.0%.") + or w:match("192%.168%.") + or w:match("10%.") + or w:match("172%.1[6-9]%.") + or w:match("172%.2[0-9]%.") + or w:match("172%.3[0-1]%.") + if not is_local then + server_param = server_param .. " -proxy " .. proxy_server_name + end + + server_param = server_param .. " -group " .. REMOTE_GROUP .. " -exclude-default-group" + if SUBNET and SUBNET ~= "" and SUBNET ~= "0" then + server_param = server_param .. " -subnet " .. SUBNET end - server_param = string.format(server_param, server_dns) table.insert(config_lines, server_param) - end) + end REMOTE_FAKEDNS = 0 else local server_param = string.format("server %s -group %s -exclude-default-group", TUN_DNS:gsub("#", ":"), REMOTE_GROUP) diff --git a/shadowsocks-rust/Cargo.lock b/shadowsocks-rust/Cargo.lock index f411784be9..4d352a6a74 100644 --- a/shadowsocks-rust/Cargo.lock +++ b/shadowsocks-rust/Cargo.lock @@ -1984,9 +1984,9 @@ dependencies = [ [[package]] name = "libmimalloc-sys" -version = "0.1.42" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4" +checksum = "bf88cd67e9de251c1781dbe2f641a1a3ad66eaae831b8a2c38fbdc5ddae16d4d" dependencies = [ "cc", "libc", @@ -2162,9 +2162,9 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "mimalloc" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af" +checksum = "b1791cbe101e95af5764f06f20f6760521f7158f69dbf9d6baf941ee1bf6bc40" dependencies = [ "libmimalloc-sys", ] @@ -3359,7 +3359,7 @@ dependencies = [ "tokio-tfo", "trait-variant", "url", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3478,7 +3478,7 @@ dependencies = [ "trait-variant", "tun", "webpki-roots 1.0.0", - "windows-sys 0.59.0", + "windows-sys 0.60.2", "zstd", ] diff --git a/shadowsocks-rust/crates/shadowsocks-service/Cargo.toml b/shadowsocks-rust/crates/shadowsocks-service/Cargo.toml index b4e4e1ac83..5d909ec0e4 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/Cargo.toml +++ b/shadowsocks-rust/crates/shadowsocks-service/Cargo.toml @@ -202,7 +202,7 @@ shadowsocks = { version = "1.23.1", path = "../shadowsocks", default-features = nix = { version = "0.30", features = ["ioctl"] } [target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.59", features = ["Win32_Networking_WinSock"] } +windows-sys = { version = "0.60", features = ["Win32_Networking_WinSock"] } [target.'cfg(any(target_os = "ios", target_os = "macos", target_os = "linux", target_os = "android", target_os = "windows", target_os = "freebsd"))'.dependencies] tun = { version = "0.8", optional = true, features = ["async"] } diff --git a/shadowsocks-rust/crates/shadowsocks/Cargo.toml b/shadowsocks-rust/crates/shadowsocks/Cargo.toml index 4b7fbcc3d0..c42276384d 100644 --- a/shadowsocks-rust/crates/shadowsocks/Cargo.toml +++ b/shadowsocks-rust/crates/shadowsocks/Cargo.toml @@ -97,7 +97,7 @@ shadowsocks-crypto = { version = "0.6.0", default-features = false } tokio-tfo = "0.3" [target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.59", features = [ +windows-sys = { version = "0.60", features = [ "Win32_Foundation", "Win32_NetworkManagement_IpHelper", "Win32_NetworkManagement_Ndis", diff --git a/shadowsocks-rust/crates/shadowsocks/src/net/sys/windows/mod.rs b/shadowsocks-rust/crates/shadowsocks/src/net/sys/windows/mod.rs index ad1be8b676..ee3b85cc22 100644 --- a/shadowsocks-rust/crates/shadowsocks/src/net/sys/windows/mod.rs +++ b/shadowsocks-rust/crates/shadowsocks/src/net/sys/windows/mod.rs @@ -26,7 +26,7 @@ use tokio::{ use tokio_tfo::TfoStream; use windows_sys::{ Win32::{ - Foundation::{BOOL, ERROR_BUFFER_OVERFLOW, ERROR_NO_DATA, ERROR_SUCCESS}, + Foundation::{ERROR_BUFFER_OVERFLOW, ERROR_NO_DATA, ERROR_SUCCESS, FALSE}, NetworkManagement::IpHelper::{ GAA_FLAG_SKIP_ANYCAST, GAA_FLAG_SKIP_DNS_SERVER, GAA_FLAG_SKIP_MULTICAST, GAA_FLAG_SKIP_UNICAST, GetAdaptersAddresses, IP_ADAPTER_ADDRESSES_LH, if_nametoindex, @@ -37,12 +37,9 @@ use windows_sys::{ WSAIoctl, htonl, setsockopt, }, }, - core::PCSTR, + core::{BOOL, PCSTR}, }; -// BOOL values -const FALSE: BOOL = 0; - use crate::net::{ AcceptOpts, AddrFamily, ConnectOpts, is_dual_stack_addr, sys::{set_common_sockopt_for_connect, socket_bind_dual_stack}, @@ -220,9 +217,7 @@ fn find_adapter_interface_index(addr: &SocketAddr, iface: &str) -> io::Result return Ok(None), _ => { - let err = io::Error::other( - format!("GetAdaptersAddresses failed with error: {}", ret), - ); + let err = io::Error::other(format!("GetAdaptersAddresses failed with error: {}", ret)); return Err(err); } } diff --git a/sing-box/common/convertor/adguard/convertor.go b/sing-box/common/convertor/adguard/convertor.go index 088635957a..40fcc9f66b 100644 --- a/sing-box/common/convertor/adguard/convertor.go +++ b/sing-box/common/convertor/adguard/convertor.go @@ -37,7 +37,15 @@ func ToOptions(reader io.Reader, logger logger.Logger) ([]option.HeadlessRule, e parseLine: for scanner.Scan() { ruleLine := scanner.Text() - if ruleLine == "" || strings.HasPrefix(ruleLine, "!") || strings.HasPrefix(ruleLine, "#") { + if ruleLine == "" { + continue + } + if strings.Contains(ruleLine, "!") { + continue + } + if strings.Contains(ruleLine, "#") { + ignoredLines++ + logger.Debug("ignored unsupported cosmetic filter: ", ruleLine) continue } originRuleLine := ruleLine diff --git a/sing-box/docs/changelog.md b/sing-box/docs/changelog.md index 2f526098d8..c800b3d450 100644 --- a/sing-box/docs/changelog.md +++ b/sing-box/docs/changelog.md @@ -2,7 +2,7 @@ icon: material/alert-decagram --- -#### 1.12.0-beta.25 +#### 1.12.0-beta.26 * Fixes and improvements diff --git a/sing-box/go.mod b/sing-box/go.mod index 700b35bad2..a45509b5c9 100644 --- a/sing-box/go.mod +++ b/sing-box/go.mod @@ -34,7 +34,7 @@ require ( github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 - github.com/sagernet/sing-tun v0.6.8-0.20250615093440-d1f6001b58c2 + github.com/sagernet/sing-tun v0.6.9-0.20250617062442-df4458520f26 github.com/sagernet/sing-vmess v0.2.4-0.20250605032146-38cc72672c88 github.com/sagernet/smux v1.5.34-mod.2 github.com/sagernet/tailscale v1.80.3-mod.5 diff --git a/sing-box/go.sum b/sing-box/go.sum index 6c7eafaf34..6f25588abd 100644 --- a/sing-box/go.sum +++ b/sing-box/go.sum @@ -180,8 +180,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= -github.com/sagernet/sing-tun v0.6.8-0.20250615093440-d1f6001b58c2 h1:leio5dGtYCKHQam+SyLszq4bbXGaxF4ElK5Aif/lUb8= -github.com/sagernet/sing-tun v0.6.8-0.20250615093440-d1f6001b58c2/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE= +github.com/sagernet/sing-tun v0.6.9-0.20250617062442-df4458520f26 h1:N9yAoqtQlawlJzLONjdQvIO3GJLEg9tZBfyKToSi0cM= +github.com/sagernet/sing-tun v0.6.9-0.20250617062442-df4458520f26/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE= github.com/sagernet/sing-vmess v0.2.4-0.20250605032146-38cc72672c88 h1:0pVm8sPOel+BoiCddW3pV3cKDKEaSioVTYDdTSKjyFI= github.com/sagernet/sing-vmess v0.2.4-0.20250605032146-38cc72672c88/go.mod h1:IL8Rr+EGwuqijszZkNrEFTQDKhilEpkqFqOlvdpS6/w= github.com/sagernet/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4= diff --git a/small/luci-app-nikki/Makefile b/small/luci-app-nikki/Makefile index 2aab787e32..9e02fff725 100644 --- a/small/luci-app-nikki/Makefile +++ b/small/luci-app-nikki/Makefile @@ -1,6 +1,6 @@ include $(TOPDIR)/rules.mk -PKG_VERSION:=1.23.0 +PKG_VERSION:=1.23.1 LUCI_TITLE:=LuCI Support for nikki LUCI_DEPENDS:=+luci-base +nikki diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua index cc9a5dbbc5..3e2a20a30d 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua @@ -459,21 +459,6 @@ if api.is_finded("smartdns") then end return DynamicList.write(self, section, t) end - function o.validate(self, value) --禁止私有IP - if type(value) == "table" then - for _, v in ipairs(value) do - if v:match("127%.0%.0%.") or - v:match("192%.168%.") or - v:match("10%.") or - v:match("172%.1[6-9]%.") or - v:match("172%.2[0-9]%.") or - v:match("172%.3[0-1]%.") then - return nil, translatef("Private IPs are not allowed: %s", v) - end - end - end - return value - end end o = s:taboption("DNS", ListValue, "xray_dns_mode", translate("Request protocol")) @@ -573,8 +558,7 @@ o.description = translate("Notify the DNS server when the DNS query is notified, o.datatype = "ipaddr" o:depends({dns_mode = "sing-box"}) o:depends({dns_mode = "xray"}) -o:depends("smartdns_dns_mode", "sing-box") -o:depends("smartdns_dns_mode", "xray") +o:depends("dns_shunt", "smartdns") o = s:taboption("DNS", Flag, "remote_fakedns", "FakeDNS", translate("Use FakeDNS work in the shunt domain that proxy.")) o.default = "0" diff --git a/small/luci-app-passwall/po/zh-cn/passwall.po b/small/luci-app-passwall/po/zh-cn/passwall.po index c3085d1fa6..c9a1fb5c46 100644 --- a/small/luci-app-passwall/po/zh-cn/passwall.po +++ b/small/luci-app-passwall/po/zh-cn/passwall.po @@ -118,9 +118,6 @@ msgstr "国内分组名" msgid "You only need to configure domestic DNS packets in SmartDNS and set it redirect or as Dnsmasq upstream, and fill in the domestic DNS group name here." msgstr "您只需要在SmartDNS配置好国内DNS分组,并设置重定向或作为Dnsmasq上游,此处填入国内DNS分组名。" -msgid "Private IPs are not allowed: %s" -msgstr "不允许使用私有 IP 地址:%s" - msgid "Filter Mode" msgstr "过滤模式" diff --git a/small/luci-app-passwall/root/usr/share/passwall/app.sh b/small/luci-app-passwall/root/usr/share/passwall/app.sh index 5d00dc0ed4..3886e87776 100755 --- a/small/luci-app-passwall/root/usr/share/passwall/app.sh +++ b/small/luci-app-passwall/root/usr/share/passwall/app.sh @@ -1610,12 +1610,13 @@ start_dns() { else smartdns_remote_dns="tcp://1.1.1.1" fi + local subnet_ip=$(config_t_get global remote_dns_client_ip) lua $APP_PATH/helper_smartdns_add.lua -FLAG "default" -SMARTDNS_CONF "/tmp/etc/smartdns/$CONFIG.conf" \ -LOCAL_GROUP ${group_domestic:-nil} -REMOTE_GROUP "passwall_proxy" -REMOTE_PROXY_SERVER ${TCP_SOCKS_server} -USE_DEFAULT_DNS "${USE_DEFAULT_DNS:-direct}" \ -REMOTE_DNS ${smartdns_remote_dns} -DNS_MODE ${DNS_MODE:-socks} -TUN_DNS ${TUN_DNS} -REMOTE_FAKEDNS ${fakedns:-0} \ -USE_DIRECT_LIST "${USE_DIRECT_LIST}" -USE_PROXY_LIST "${USE_PROXY_LIST}" -USE_BLOCK_LIST "${USE_BLOCK_LIST}" -USE_GFW_LIST "${USE_GFW_LIST}" -CHN_LIST "${CHN_LIST}" \ -TCP_NODE ${TCP_NODE} -DEFAULT_PROXY_MODE "${TCP_PROXY_MODE}" -NO_PROXY_IPV6 ${FILTER_PROXY_IPV6:-0} -NFTFLAG ${nftflag:-0} \ - -NO_LOGIC_LOG ${NO_LOGIC_LOG:-0} + -SUBNET ${subnet_ip:-0} -NO_LOGIC_LOG ${NO_LOGIC_LOG:-0} source $APP_PATH/helper_smartdns.sh restart echolog " - 域名解析:使用SmartDNS,请确保配置正常。" return diff --git a/small/luci-app-passwall/root/usr/share/passwall/helper_smartdns_add.lua b/small/luci-app-passwall/root/usr/share/passwall/helper_smartdns_add.lua index 7aa21a9381..919bfeb8e0 100644 --- a/small/luci-app-passwall/root/usr/share/passwall/helper_smartdns_add.lua +++ b/small/luci-app-passwall/root/usr/share/passwall/helper_smartdns_add.lua @@ -23,6 +23,7 @@ local DEFAULT_PROXY_MODE = var["-DEFAULT_PROXY_MODE"] local NO_PROXY_IPV6 = var["-NO_PROXY_IPV6"] local NO_LOGIC_LOG = var["-NO_LOGIC_LOG"] local NFTFLAG = var["-NFTFLAG"] +local SUBNET = var["-SUBNET"] local uci = api.uci local sys = api.sys @@ -168,56 +169,68 @@ config_lines = { DNS_MODE == "socks" and string.format("proxy-server socks5://%s -name %s", REMOTE_PROXY_SERVER, proxy_server_name) or nil } if DNS_MODE == "socks" then - local found_private = false - for dns in string.gmatch(REMOTE_DNS, "([^|]+)") do - -- 判断是否为私有地址 - if dns:match("127%.0%.0%.") or - dns:match("192%.168%.") or - dns:match("10%.") or - dns:match("172%.1[6-9]%.") or - dns:match("172%.2[0-9]%.") or - dns:match("172%.3[0-1]%.") then - found_private = true - break - end - end - if found_private then - REMOTE_DNS = "tcp://1.1.1.1" - log(" * 错误!SmartDNS 远程 DNS 不允许使用私有地址,将默认使用:tcp://1.1.1.1") - end - string.gsub(REMOTE_DNS, '[^' .. "|" .. ']+', function(w) - local server_dns = w - local server_param = string.format("server %s -group %s -proxy %s", "%s", REMOTE_GROUP, proxy_server_name) - server_param = server_param .. " -exclude-default-group" + for w in string.gmatch(REMOTE_DNS, '[^|]+') do + local server_dns = api.trim(w) + local server_param - local isHTTPS = w:find("https://") - if isHTTPS and isHTTPS == 1 then - local http_host = nil - local url = w - local port = 443 - local s = api.split(w, ",") - if s and #s > 1 then - url = s[1] - local dns_ip = s[2] - local host_port = api.get_domain_from_url(s[1]) - if host_port and #host_port > 0 then - http_host = host_port - local s2 = api.split(host_port, ":") - if s2 and #s2 > 1 then - http_host = s2[1] - port = s2[2] - end - url = url:gsub(http_host, dns_ip) + local dnsType = string.match(server_dns, "^(.-)://") + dnsType = dnsType and string.lower(dnsType) or nil + local dnsServer = string.match(server_dns, "://(.+)") or server_dns + + if dnsType and dnsType ~= "" and dnsType ~= "udp" then + if dnsType == "tcp" then + server_param = "server-tcp " .. dnsServer + elseif dnsType == "tls" then + server_param = "server-tls " .. dnsServer + elseif dnsType == "quic" then + server_param = "server-quic " .. dnsServer + elseif dnsType == "https" or dnsType == "h3" then + local http_host = nil + local url = w + local port = 443 + local s = api.split(w, ",") + if s and #s > 1 then + url = s[1] + local dns_ip = s[2] + local host_port = api.get_domain_from_url(s[1]) + if host_port and #host_port > 0 then + http_host = host_port + local s2 = api.split(host_port, ":") + if s2 and #s2 > 1 then + http_host = s2[1] + port = s2[2] + end + url = url:gsub(http_host, dns_ip) + end end + server_dns = url + if http_host then + server_dns = server_dns .. " -http-host " .. http_host + end + server_param = (dnsType == "https" and "server-https " or "server-h3 ") .. server_dns end - server_dns = url - if http_host then - server_dns = server_dns .. " -http-host " .. http_host - end + else + server_param = "server " .. dnsServer + + end + + -- 判断是否为本地地址 + local is_local = w:match("127%.0%.0%.") + or w:match("192%.168%.") + or w:match("10%.") + or w:match("172%.1[6-9]%.") + or w:match("172%.2[0-9]%.") + or w:match("172%.3[0-1]%.") + if not is_local then + server_param = server_param .. " -proxy " .. proxy_server_name + end + + server_param = server_param .. " -group " .. REMOTE_GROUP .. " -exclude-default-group" + if SUBNET and SUBNET ~= "" and SUBNET ~= "0" then + server_param = server_param .. " -subnet " .. SUBNET end - server_param = string.format(server_param, server_dns) table.insert(config_lines, server_param) - end) + end REMOTE_FAKEDNS = 0 else local server_param = string.format("server %s -group %s -exclude-default-group", TUN_DNS:gsub("#", ":"), REMOTE_GROUP) diff --git a/small/nikki/Makefile b/small/nikki/Makefile index 4c816dd155..547f0254c6 100644 --- a/small/nikki/Makefile +++ b/small/nikki/Makefile @@ -5,9 +5,9 @@ PKG_RELEASE:=1 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/MetaCubeX/mihomo.git -PKG_SOURCE_DATE:=2025-06-12 -PKG_SOURCE_VERSION:=32d447ce99f24626265b55cdf4ce73f9bb8ec5c3 -PKG_MIRROR_HASH:=51cb25faf59277cc7f9c066dc5f2ba9ad48558736812401a728765f345918a9d +PKG_SOURCE_DATE:=2025-06-14 +PKG_SOURCE_VERSION:=c60750d5491994f1aa506a2d282a668933c4838c +PKG_MIRROR_HASH:=359269a75c325c72bab7753b2f1db523f57df7a7521cf6bd8962e3a05ace01fe PKG_LICENSE:=GPL3.0+ PKG_MAINTAINER:=Joseph Mory @@ -16,7 +16,7 @@ PKG_BUILD_DEPENDS:=golang/host PKG_BUILD_PARALLEL:=1 PKG_BUILD_FLAGS:=no-mips16 -PKG_BUILD_VERSION:=alpha-32d447c +PKG_BUILD_VERSION:=alpha-c60750d PKG_BUILD_TIME:=$(shell date -u -Iseconds) GO_PKG:=github.com/metacubex/mihomo diff --git a/small/nikki/files/nikki.conf b/small/nikki/files/nikki.conf index bcdf5c9635..78385fd98b 100644 --- a/small/nikki/files/nikki.conf +++ b/small/nikki/files/nikki.conf @@ -94,14 +94,14 @@ config router_access_control list 'group' 'nogroup' list 'group' 'ntp' list 'group' 'ubus' - list 'cgroup' 'adguardhome' - list 'cgroup' 'aria2' - list 'cgroup' 'dnsmasq' - list 'cgroup' 'netbird' - list 'cgroup' 'qbittorrent' - list 'cgroup' 'sysntpd' - list 'cgroup' 'tailscale' - list 'cgroup' 'zerotier' + list 'cgroup' 'services/adguardhome' + list 'cgroup' 'services/aria2' + list 'cgroup' 'services/dnsmasq' + list 'cgroup' 'services/netbird' + list 'cgroup' 'services/qbittorrent' + list 'cgroup' 'services/sysntpd' + list 'cgroup' 'services/tailscale' + list 'cgroup' 'services/zerotier' option 'proxy' '0' config router_access_control diff --git a/small/nikki/files/nikki.init b/small/nikki/files/nikki.init index af90029f02..625c7b118d 100644 --- a/small/nikki/files/nikki.init +++ b/small/nikki/files/nikki.init @@ -210,14 +210,14 @@ service_started() { config_get tun_interval "proxy" "tun_interval" 1 ## routing config local tproxy_fw_mark tun_fw_mark tproxy_rule_pref tun_rule_pref tproxy_route_table tun_route_table cgroup_id cgroup_name - config_get tproxy_fw_mark "routing" "tproxy_fw_mark" - config_get tun_fw_mark "routing" "tun_fw_mark" - config_get tproxy_rule_pref "routing" "tproxy_rule_pref" - config_get tun_rule_pref "routing" "tun_rule_pref" - config_get tproxy_route_table "routing" "tproxy_route_table" - config_get tun_route_table "routing" "tun_route_table" - config_get cgroup_id "routing" "cgroup_id" - config_get cgroup_name "routing" "cgroup_name" + config_get tproxy_fw_mark "routing" "tproxy_fw_mark" "0x80" + config_get tun_fw_mark "routing" "tun_fw_mark" "0x81" + config_get tproxy_rule_pref "routing" "tproxy_rule_pref" "1024" + config_get tun_rule_pref "routing" "tun_rule_pref" "1025" + config_get tproxy_route_table "routing" "tproxy_route_table" "80" + config_get tun_route_table "routing" "tun_route_table" "81" + config_get cgroup_id "routing" "cgroup_id" "0x12061206" + config_get cgroup_name "routing" "cgroup_name" "nikki" # prepare config local tproxy_enable; tproxy_enable=0 if [[ "$tcp_mode" == "tproxy" || "$udp_mode" == "tproxy" ]]; then @@ -330,8 +330,8 @@ cleanup() { # get config ## routing config local tproxy_route_table tun_route_table - config_get tproxy_route_table "routing" "tproxy_route_table" - config_get tun_route_table "routing" "tun_route_table" + config_get tproxy_route_table "routing" "tproxy_route_table" "80" + config_get tun_route_table "routing" "tun_route_table" "81" # delete routing policy ip -4 rule del table "$tproxy_route_table" > /dev/null 2>&1 ip -4 rule del table "$tun_route_table" > /dev/null 2>&1 diff --git a/small/nikki/files/uci-defaults/migrate.sh b/small/nikki/files/uci-defaults/migrate.sh index 34beb1d89d..3044a7f213 100644 --- a/small/nikki/files/uci-defaults/migrate.sh +++ b/small/nikki/files/uci-defaults/migrate.sh @@ -127,6 +127,16 @@ proxy_tun_timeout=$(uci -q get nikki.proxy.tun_timeout); [ -z "$proxy_tun_timeou proxy_tun_interval=$(uci -q get nikki.proxy.tun_interval); [ -z "$proxy_tun_interval" ] && uci set nikki.proxy.tun_interval=1 +# since v1.23.1 +while read router_access_control; do + for cgroup in $(uci -q get "$router_access_control.cgroup"); do + uci del_list "$router_access_control.cgroup=$cgroup" + [ ! -d "/sys/fs/cgroup/$cgroup" ] && [ -d "/sys/fs/cgroup/services/$cgroup" ] && { + uci add_list "$router_access_control.cgroup=services/$cgroup" + } + done +done < <(uci show nikki | grep -o -E 'nikki.@router_access_control\[[[:digit:]]+\]=router_access_control' | cut -d '=' -f 1) + # commit uci commit nikki diff --git a/small/nikki/files/ucode/hijack.ut b/small/nikki/files/ucode/hijack.ut index f9461b59f2..97a6d21837 100644 --- a/small/nikki/files/ucode/hijack.ut +++ b/small/nikki/files/ucode/hijack.ut @@ -70,10 +70,10 @@ const proxy_tcp_dport = split((uci.get('nikki', 'proxy', 'proxy_tcp_dport') ?? '0-65535'), ' '); const proxy_udp_dport = split((uci.get('nikki', 'proxy', 'proxy_udp_dport') ?? '0-65535'), ' '); - const cgroup_id = uci.get('nikki', 'routing', 'cgroup_id'); - const cgroup_name = uci.get('nikki', 'routing', 'cgroup_name'); - const tproxy_fw_mark = uci.get('nikki', 'routing', 'tproxy_fw_mark'); - const tun_fw_mark = uci.get('nikki', 'routing', 'tun_fw_mark'); + const cgroup_id = uci.get('nikki', 'routing', 'cgroup_id') ?? '0x12061206'; + const cgroup_name = uci.get('nikki', 'routing', 'cgroup_name') ?? 'nikki'; + const tproxy_fw_mark = uci.get('nikki', 'routing', 'tproxy_fw_mark') ?? '80'; + const tun_fw_mark = uci.get('nikki', 'routing', 'tun_fw_mark') ?? '81'; const dns_hijack_nfproto = []; if (ipv4_dns_hijack) { @@ -106,9 +106,7 @@ table inet nikki { flags interval {% if (length(dns_hijack_nfproto) > 0): %} elements = { - {% for (let nfproto in dns_hijack_nfproto): %} - {{ nfproto }}, - {% endfor %} + {{ join(', ', dns_hijack_nfproto) }} } {% endif %} } @@ -118,9 +116,7 @@ table inet nikki { flags interval {% if (length(proxy_nfproto) > 0): %} elements = { - {% for (let nfproto in proxy_nfproto): %} - {{ nfproto }}, - {% endfor %} + {{ join(', ', proxy_nfproto) }} } {% endif %} } @@ -143,9 +139,7 @@ table inet nikki { auto-merge {% if (length(lan_inbound_device) > 0): %} elements = { - {% for (let device in lan_inbound_device): %} - "{{ device }}", - {% endfor %} + {{ join(', ', map(lan_inbound_device, (x) => `"${x}"`)) }} } {% endif %} } @@ -166,9 +160,7 @@ table inet nikki { auto-merge {% if (length(proxy_dport) > 0): %} elements = { - {% for (let dport in proxy_dport): %} - {{ dport }}, - {% endfor %} + {{ join(', ', proxy_dport) }} } {% endif %} } @@ -179,9 +171,7 @@ table inet nikki { auto-merge {% if (length(bypass_dscp) > 0): %} elements = { - {% for (let dscp in bypass_dscp): %} - {{ dscp }}, - {% endfor %} + {{ join(', ', bypass_dscp) }} } {% endif %} } @@ -195,16 +185,18 @@ table inet nikki { {% else %} {% if (length(access_control['user']) > 0): %} - meta l4proto { tcp, udp } meta skuid { {% for (let user in access_control['user']): %} {{ user }}, {% endfor %} } th dport 53 counter {% if (access_control.proxy == '1'): %} redirect to :{{ dns_port }} {% else %} return {% endif %} + meta l4proto { tcp, udp } meta skuid { {{ join(', ', access_control['user']) }} } th dport 53 counter {% if (access_control.proxy == '1'): %} redirect to :{{ dns_port }} {% else %} return {% endif %} {% endif %} {% if (length(access_control['group']) > 0): %} - meta l4proto { tcp, udp } meta skgid { {% for (let group in access_control['group']): %} {{ group }}, {% endfor %} } th dport 53 counter {% if (access_control.proxy == '1'): %} redirect to :{{ dns_port }} {% else %} return {% endif %} + meta l4proto { tcp, udp } meta skgid { {{ join(', ', access_control['group']) }} } th dport 53 counter {% if (access_control.proxy == '1'): %} redirect to :{{ dns_port }} {% else %} return {% endif %} {% endif %} {% if (cgroups_version == 2 && length(access_control['cgroup']) > 0): %} - meta l4proto { tcp, udp } socket cgroupv2 level 2 { {% for (let cgroup in access_control['cgroup']): %} services/{{ cgroup }}, {% endfor %} } th dport 53 counter {% if (access_control.proxy == '1'): %} redirect to :{{ dns_port }} {% else %} return {% endif %} - + {% for (let cgroup in access_control['cgroup']): %} + meta l4proto { tcp, udp } socket cgroupv2 level {{ length(split(cgroup, '/')) }} "{{ cgroup }}" th dport 53 counter {% if (access_control.proxy == '1'): %} redirect to :{{ dns_port }} {% else %} return {% endif %} + + {% endfor %} {% endif %} {% endif %} {% endif %} @@ -219,16 +211,18 @@ table inet nikki { {% else %} {% if (length(access_control['user']) > 0): %} - meta l4proto tcp meta skuid { {% for (let user in access_control['user']): %} {{ user }}, {% endfor %} } counter {% if (access_control.proxy == '1'): %} redirect to :{{ redir_port }} {% else %} return {% endif %} + meta l4proto tcp meta skuid { {{ join(', ', access_control['user']) }} } counter {% if (access_control.proxy == '1'): %} redirect to :{{ redir_port }} {% else %} return {% endif %} {% endif %} {% if (length(access_control['group']) > 0): %} - meta l4proto tcp meta skgid { {% for (let group in access_control['group']): %} {{ group }}, {% endfor %} } counter {% if (access_control.proxy == '1'): %} redirect to :{{ redir_port }} {% else %} return {% endif %} + meta l4proto tcp meta skgid { {{ join(', ', access_control['group']) }} } counter {% if (access_control.proxy == '1'): %} redirect to :{{ redir_port }} {% else %} return {% endif %} {% endif %} {% if (cgroups_version == 2 && length(access_control['cgroup']) > 0): %} - meta l4proto tcp socket cgroupv2 level 2 { {% for (let cgroup in access_control['cgroup']): %} services/{{ cgroup }}, {% endfor %} } counter {% if (access_control.proxy == '1'): %} redirect to :{{ redir_port }} {% else %} return {% endif %} - + {% for (let cgroup in access_control['cgroup']): %} + meta l4proto tcp socket cgroupv2 level {{ length(split(cgroup, '/')) }} "{{ cgroup }}" counter {% if (access_control.proxy == '1'): %} redirect to :{{ redir_port }} {% else %} return {% endif %} + + {% endfor %} {% endif %} {% endif %} {% endif %} @@ -243,16 +237,18 @@ table inet nikki { {% else %} {% if (length(access_control['user']) > 0): %} - meta l4proto { tcp, udp } meta skuid { {% for (let user in access_control['user']): %} {{ user }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tproxy_fw_mark }} counter accept {% else %} counter return {% endif %} + meta l4proto { tcp, udp } meta skuid { {{ join(', ', access_control['user']) }} } {% if (access_control.proxy == '1'): %} meta mark set {{ tproxy_fw_mark }} counter accept {% else %} counter return {% endif %} {% endif %} {% if (length(access_control['group']) > 0): %} - meta l4proto { tcp, udp } meta skgid { {% for (let group in access_control['group']): %} {{ group }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tproxy_fw_mark }} counter accept {% else %} counter return {% endif %} + meta l4proto { tcp, udp } meta skgid { {{ join(', ', access_control['group']) }} } {% if (access_control.proxy == '1'): %} meta mark set {{ tproxy_fw_mark }} counter accept {% else %} counter return {% endif %} {% endif %} {% if (cgroups_version == 2 && length(access_control['cgroup']) > 0): %} - meta l4proto { tcp, udp } socket cgroupv2 level 2 { {% for (let cgroup in access_control['cgroup']): %} services/{{ cgroup }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tproxy_fw_mark }} counter accept {% else %} counter return {% endif %} - + {% for (let cgroup in access_control['cgroup']): %} + meta l4proto { tcp, udp } socket cgroupv2 level {{ length(split(cgroup, '/')) }} "{{ cgroup }}" {% if (access_control.proxy == '1'): %} meta mark set {{ tproxy_fw_mark }} counter accept {% else %} counter return {% endif %} + + {% endfor %} {% endif %} {% endif %} {% endif %} @@ -267,16 +263,18 @@ table inet nikki { {% else %} {% if (length(access_control['user']) > 0): %} - meta l4proto { tcp, udp } meta skuid { {% for (let user in access_control['user']): %} {{ user }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tun_fw_mark }} counter accept {% else %} counter return {% endif %} + meta l4proto { tcp, udp } meta skuid { {{ join(', ', access_control['user']) }} } {% if (access_control.proxy == '1'): %} meta mark set {{ tun_fw_mark }} counter accept {% else %} counter return {% endif %} {% endif %} {% if (length(access_control['group']) > 0): %} - meta l4proto { tcp, udp } meta skgid { {% for (let group in access_control['group']): %} {{ group }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tun_fw_mark }} counter accept {% else %} counter return {% endif %} + meta l4proto { tcp, udp } meta skgid { {{ join(', ', access_control['group']) }} } {% if (access_control.proxy == '1'): %} meta mark set {{ tun_fw_mark }} counter accept {% else %} counter return {% endif %} {% endif %} {% if (cgroups_version == 2 && length(access_control['cgroup']) > 0): %} - meta l4proto { tcp, udp } socket cgroupv2 level 2 { {% for (let cgroup in access_control['cgroup']): %} services/{{ cgroup }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tun_fw_mark }} counter accept {% else %} counter return {% endif %} - + {% for (let cgroup in access_control['cgroup']): %} + meta l4proto { tcp, udp } socket cgroupv2 level {{ length(split(cgroup, '/')) }} "{{ cgroup }}" {% if (access_control.proxy == '1'): %} meta mark set {{ tun_fw_mark }} counter accept {% else %} counter return {% endif %} + + {% endfor %} {% endif %} {% endif %} {% endif %} @@ -293,15 +291,15 @@ table inet nikki { {% else %} {% if (length(access_control['ip']) > 0): %} - meta l4proto { tcp, udp } ip saddr { {% for (let ip in access_control['ip']): %} {{ ip }}, {% endfor %} } th dport 53 counter {% if (access_control.proxy == '1'): %} redirect to :{{ dns_port }} {% else %} return {% endif %} + meta l4proto { tcp, udp } ip saddr { {{ join(', ', access_control['ip']) }} } th dport 53 counter {% if (access_control.proxy == '1'): %} redirect to :{{ dns_port }} {% else %} return {% endif %} {% endif %} {% if (length(access_control['ip6']) > 0): %} - meta l4proto { tcp, udp } ip6 saddr { {% for (let ip6 in access_control['ip6']): %} {{ ip6 }}, {% endfor %} } th dport 53 counter {% if (access_control.proxy == '1'): %} redirect to :{{ dns_port }} {% else %} return {% endif %} + meta l4proto { tcp, udp } ip6 saddr { {{ join(', ', access_control['ip6']) }} } th dport 53 counter {% if (access_control.proxy == '1'): %} redirect to :{{ dns_port }} {% else %} return {% endif %} {% endif %} {% if (length(access_control['mac']) > 0): %} - meta l4proto { tcp, udp } ether saddr { {% for (let mac in access_control['mac']): %} {{ mac }}, {% endfor %} } th dport 53 counter {% if (access_control.proxy == '1'): %} redirect to :{{ dns_port }} {% else %} return {% endif %} + meta l4proto { tcp, udp } ether saddr { {{ join(', ', access_control['mac']) }} } th dport 53 counter {% if (access_control.proxy == '1'): %} redirect to :{{ dns_port }} {% else %} return {% endif %} {% endif %} {% endif %} @@ -317,15 +315,15 @@ table inet nikki { {% else %} {% if (length(access_control['ip']) > 0): %} - meta l4proto tcp ip saddr { {% for (let ip in access_control['ip']): %} {{ ip }}, {% endfor %} } counter {% if (access_control.proxy == '1'): %} redirect to :{{ redir_port }} {% else %} return {% endif %} + meta l4proto tcp ip saddr { {{ join(', ', access_control['ip']) }} } counter {% if (access_control.proxy == '1'): %} redirect to :{{ redir_port }} {% else %} return {% endif %} {% endif %} {% if (length(access_control['ip6']) > 0): %} - meta l4proto tcp ip6 saddr { {% for (let ip6 in access_control['ip6']): %} {{ ip6 }}, {% endfor %} } counter {% if (access_control.proxy == '1'): %} redirect to :{{ redir_port }} {% else %} return {% endif %} + meta l4proto tcp ip6 saddr { {{ join(', ', access_control['ip6']) }} } counter {% if (access_control.proxy == '1'): %} redirect to :{{ redir_port }} {% else %} return {% endif %} {% endif %} {% if (length(access_control['mac']) > 0): %} - meta l4proto tcp ether saddr { {% for (let mac in access_control['mac']): %} {{ mac }}, {% endfor %} } counter {% if (access_control.proxy == '1'): %} redirect to :{{ redir_port }} {% else %} return {% endif %} + meta l4proto tcp ether saddr { {{ join(', ', access_control['mac']) }} } counter {% if (access_control.proxy == '1'): %} redirect to :{{ redir_port }} {% else %} return {% endif %} {% endif %} {% endif %} @@ -341,15 +339,15 @@ table inet nikki { {% else %} {% if (length(access_control['ip']) > 0): %} - meta l4proto { tcp, udp } ip saddr { {% for (let ip in access_control['ip']): %} {{ ip }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tproxy_fw_mark }} tproxy ip to :{{ tproxy_port }} counter accept {% else %} counter return {% endif %} + meta l4proto { tcp, udp } ip saddr { {{ join(', ', access_control['ip']) }} } {% if (access_control.proxy == '1'): %} meta mark set {{ tproxy_fw_mark }} tproxy ip to :{{ tproxy_port }} counter accept {% else %} counter return {% endif %} {% endif %} {% if (length(access_control['ip6']) > 0): %} - meta l4proto { tcp, udp } ip6 saddr { {% for (let ip6 in access_control['ip6']): %} {{ ip6 }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tproxy_fw_mark }} tproxy ip6 to :{{ tproxy_port }} counter accept {% else %} counter return {% endif %} + meta l4proto { tcp, udp } ip6 saddr { {{ join(', ', access_control['ip6']) }} } {% if (access_control.proxy == '1'): %} meta mark set {{ tproxy_fw_mark }} tproxy ip6 to :{{ tproxy_port }} counter accept {% else %} counter return {% endif %} {% endif %} {% if (length(access_control['mac']) > 0): %} - meta l4proto { tcp, udp } ether saddr { {% for (let mac in access_control['mac']): %} {{ mac }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tproxy_fw_mark }} tproxy to :{{ tproxy_port }} counter accept {% else %} counter return {% endif %} + meta l4proto { tcp, udp } ether saddr { {{ join(', ', access_control['mac']) }} } {% if (access_control.proxy == '1'): %} meta mark set {{ tproxy_fw_mark }} tproxy to :{{ tproxy_port }} counter accept {% else %} counter return {% endif %} {% endif %} {% endif %} @@ -365,15 +363,15 @@ table inet nikki { {% else %} {% if (length(access_control['ip']) > 0): %} - meta l4proto { tcp, udp } ip saddr { {% for (let ip in access_control['ip']): %} {{ ip }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tun_fw_mark }} counter accept {% else %}counter return {% endif %} + meta l4proto { tcp, udp } ip saddr { {{ join(', ', access_control['ip']) }} } {% if (access_control.proxy == '1'): %} meta mark set {{ tun_fw_mark }} counter accept {% else %}counter return {% endif %} {% endif %} {% if (length(access_control['ip6']) > 0): %} - meta l4proto { tcp, udp } ip6 saddr { {% for (let ip6 in access_control['ip6']): %} {{ ip6 }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tun_fw_mark }} counter accept {% else %}counter return {% endif %} + meta l4proto { tcp, udp } ip6 saddr { {{ join(', ', access_control['ip6']) }} } {% if (access_control.proxy == '1'): %} meta mark set {{ tun_fw_mark }} counter accept {% else %}counter return {% endif %} {% endif %} {% if (length(access_control['mac']) > 0): %} - meta l4proto { tcp, udp } ether saddr { {% for (let mac in access_control['mac']): %} {{ mac }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tun_fw_mark }} counter accept {% else %}counter return {% endif %} + meta l4proto { tcp, udp } ether saddr { {{ join(', ', access_control['mac']) }} } {% if (access_control.proxy == '1'): %} meta mark set {{ tun_fw_mark }} counter accept {% else %}counter return {% endif %} {% endif %} {% endif %} @@ -388,11 +386,11 @@ table inet nikki { {% if (cgroups_version == 1): %} meta cgroup {{ cgroup_id }} counter return {% elif (cgroups_version == 2): %} - socket cgroupv2 level 2 services/{{ cgroup_name }} counter return + socket cgroupv2 level 2 "services/{{ cgroup_name }}" counter return {% endif %} meta nfproto @dns_hijack_nfproto jump router_dns_hijack {% if (tcp_mode == 'redirect'): %} - fib daddr type { local, multicast, broadcast, anycast } counter return + fib daddr type { local, broadcast, anycast, multicast } counter return ct direction reply counter return ip daddr @reserved_ip counter return ip6 daddr @reserved_ip6 counter return @@ -414,9 +412,9 @@ table inet nikki { {% if (cgroups_version == 1): %} meta cgroup {{ cgroup_id }} counter return {% elif (cgroups_version == 2): %} - socket cgroupv2 level 2 services/{{ cgroup_name }} counter return + socket cgroupv2 level 2 "services/{{ cgroup_name }}" counter return {% endif %} - fib daddr type { local, multicast, broadcast, anycast } counter return + fib daddr type { local, broadcast, anycast, multicast } counter return ct direction reply counter return ip daddr @reserved_ip counter return ip6 daddr @reserved_ip6 counter return @@ -455,7 +453,7 @@ table inet nikki { type nat hook prerouting priority dstnat + 1; policy accept; iifname @lan_inbound_device meta nfproto @dns_hijack_nfproto jump lan_dns_hijack {% if (tcp_mode == 'redirect'): %} - fib daddr type { local, multicast, broadcast, anycast } counter return + fib daddr type { local, broadcast, anycast, multicast } counter return ct direction reply counter return ip daddr @reserved_ip counter return ip6 daddr @reserved_ip6 counter return @@ -474,7 +472,7 @@ table inet nikki { chain mangle_prerouting_lan { type filter hook prerouting priority mangle; policy accept; - fib daddr type { local, multicast, broadcast, anycast } counter return + fib daddr type { local, broadcast, anycast, multicast } counter return ct direction reply counter return ip daddr @reserved_ip counter return ip6 daddr @reserved_ip6 counter return diff --git a/small/nikki/files/ucode/include.uc b/small/nikki/files/ucode/include.uc index 5272f18c58..a6411f2e45 100644 --- a/small/nikki/files/ucode/include.uc +++ b/small/nikki/files/ucode/include.uc @@ -1,5 +1,4 @@ -import { readfile, lsdir, lstat } from 'fs'; -import { connect } from 'ubus'; +import { readfile, popen } from 'fs'; export function uci_bool(obj) { return obj == null ? null : obj == '1'; @@ -64,12 +63,14 @@ export function get_groups() { }; export function get_cgroups() { - const ubus = connect(); - const services = ubus.call('service', 'list'); const result = []; - for (let name in services) { - if (length(services[name]['instances']) > 0) { - push(result, name); + if (get_cgroups_version() == 2) { + const cgroup_path = '/sys/fs/cgroup/'; + const process = popen(`find ${cgroup_path} -type d -mindepth 1`); + if (process) { + for (let line = process.read('line'); length(line); line = process.read('line')) { + push(result, substr(trim(line), length(cgroup_path))); + } } } return result; diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt index c0e6fae8f6..ddbc78a38e 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt @@ -828,7 +828,11 @@ object V2rayConfigManager { for (item in proxyOutboundList) { val domain = item.getServerAddress() if (domain.isNullOrEmpty()) continue - if (newHosts.containsKey(domain)) continue + + if (newHosts.containsKey(domain)) { + item.ensureSockopt().domainStrategy = if (preferIpv6) "UseIPv6v4" else "UseIPv4v6" + continue + } val resolvedIps = HttpUtil.resolveHostToIP(domain, preferIpv6) if (resolvedIps.isNullOrEmpty()) continue diff --git a/yt-dlp/test/test_traversal.py b/yt-dlp/test/test_traversal.py index bc433029d8..52215f5a7b 100644 --- a/yt-dlp/test/test_traversal.py +++ b/yt-dlp/test/test_traversal.py @@ -416,18 +416,8 @@ class TestTraversal: '`any` should allow further branching' def test_traversal_morsel(self): - values = { - 'expires': 'a', - 'path': 'b', - 'comment': 'c', - 'domain': 'd', - 'max-age': 'e', - 'secure': 'f', - 'httponly': 'g', - 'version': 'h', - 'samesite': 'i', - } morsel = http.cookies.Morsel() + values = dict(zip(morsel, 'abcdefghijklmnop')) morsel.set('item_key', 'item_value', 'coded_value') morsel.update(values) values['key'] = 'item_key'