diff --git a/.github/update.log b/.github/update.log index 062fdb2b38..0dae56d533 100644 --- a/.github/update.log +++ b/.github/update.log @@ -1133,3 +1133,4 @@ Update On Tue Sep 23 20:37:30 CEST 2025 Update On Wed Sep 24 20:34:28 CEST 2025 Update On Thu Sep 25 20:42:08 CEST 2025 Update On Fri Sep 26 20:35:20 CEST 2025 +Update On Sat Sep 27 20:33:29 CEST 2025 diff --git a/clash-nyanpasu/README.md b/clash-nyanpasu/README.md index 3f85e72233..c8cc1b092c 100644 --- a/clash-nyanpasu/README.md +++ b/clash-nyanpasu/README.md @@ -15,6 +15,7 @@ GitHub Downloads (all assets, all releases) Nyanpasu License Nyanpasu Twitter + Ask DeepWiki

## Features diff --git a/clash-nyanpasu/frontend/interface/package.json b/clash-nyanpasu/frontend/interface/package.json index 8f263337b5..0059690454 100644 --- a/clash-nyanpasu/frontend/interface/package.json +++ b/clash-nyanpasu/frontend/interface/package.json @@ -22,6 +22,6 @@ }, "devDependencies": { "@types/lodash-es": "4.17.12", - "@types/react": "19.1.12" + "@types/react": "19.1.14" } } diff --git a/clash-nyanpasu/frontend/nyanpasu/package.json b/clash-nyanpasu/frontend/nyanpasu/package.json index 2618646822..37ff152331 100644 --- a/clash-nyanpasu/frontend/nyanpasu/package.json +++ b/clash-nyanpasu/frontend/nyanpasu/package.json @@ -33,7 +33,7 @@ "dayjs": "1.11.18", "framer-motion": "12.23.22", "i18next": "25.5.2", - "jotai": "2.14.0", + "jotai": "2.15.0", "json-schema": "0.4.0", "material-react-table": "3.2.1", "monaco-editor": "0.52.2", @@ -49,7 +49,7 @@ "react-use": "17.6.0", "rxjs": "7.8.2", "swr": "2.3.6", - "virtua": "0.43.4", + "virtua": "0.43.5", "vite-bundle-visualizer": "1.2.1" }, "devDependencies": { @@ -60,7 +60,7 @@ "@monaco-editor/react": "4.7.0", "@tanstack/react-query": "5.90.2", "@tanstack/react-router": "1.132.7", - "@tanstack/react-router-devtools": "1.132.7", + "@tanstack/react-router-devtools": "1.132.11", "@tanstack/router-plugin": "1.132.7", "@tauri-apps/plugin-clipboard-manager": "2.3.0", "@tauri-apps/plugin-dialog": "2.4.0", @@ -70,7 +70,7 @@ "@tauri-apps/plugin-process": "2.3.0", "@tauri-apps/plugin-shell": "2.3.1", "@tauri-apps/plugin-updater": "2.9.0", - "@types/react": "19.1.12", + "@types/react": "19.1.14", "@types/react-dom": "19.1.9", "@types/validator": "13.15.3", "@vitejs/plugin-legacy": "7.2.1", diff --git a/clash-nyanpasu/frontend/ui/package.json b/clash-nyanpasu/frontend/ui/package.json index e2dbebe5bb..6daf1be2e5 100644 --- a/clash-nyanpasu/frontend/ui/package.json +++ b/clash-nyanpasu/frontend/ui/package.json @@ -19,7 +19,7 @@ "@radix-ui/react-scroll-area": "1.2.10", "@tauri-apps/api": "2.8.0", "@types/d3": "7.4.3", - "@types/react": "19.1.12", + "@types/react": "19.1.14", "@vitejs/plugin-react": "5.0.3", "ahooks": "3.9.5", "d3": "7.9.0", diff --git a/clash-nyanpasu/manifest/version.json b/clash-nyanpasu/manifest/version.json index 6a1cd02a39..697c6fcc7e 100644 --- a/clash-nyanpasu/manifest/version.json +++ b/clash-nyanpasu/manifest/version.json @@ -2,7 +2,7 @@ "manifest_version": 1, "latest": { "mihomo": "v1.19.14", - "mihomo_alpha": "alpha-fdc46f0", + "mihomo_alpha": "alpha-f45c6f5", "clash_rs": "v0.9.0", "clash_premium": "2023-09-05-gdcc8d87", "clash_rs_alpha": "0.9.0-alpha+sha.2784d7a" @@ -69,5 +69,5 @@ "linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf" } }, - "updated_at": "2025-09-24T22:21:01.034Z" + "updated_at": "2025-09-26T22:21:10.718Z" } diff --git a/clash-nyanpasu/package.json b/clash-nyanpasu/package.json index 9aec00d565..e4743f3eea 100644 --- a/clash-nyanpasu/package.json +++ b/clash-nyanpasu/package.json @@ -66,7 +66,7 @@ "@tauri-apps/cli": "2.8.4", "@types/fs-extra": "11.0.4", "@types/lodash-es": "4.17.12", - "@types/node": "24.3.1", + "@types/node": "24.5.2", "@typescript-eslint/eslint-plugin": "8.44.1", "@typescript-eslint/parser": "8.44.1", "autoprefixer": "10.4.21", @@ -106,7 +106,7 @@ "stylelint-order": "7.0.0", "stylelint-scss": "6.12.1", "tailwindcss": "4.1.13", - "tsx": "4.20.5", + "tsx": "4.20.6", "typescript": "5.9.2", "typescript-eslint": "8.44.1" }, diff --git a/clash-nyanpasu/pnpm-lock.yaml b/clash-nyanpasu/pnpm-lock.yaml index c14a41c6a0..7dddadd7d2 100644 --- a/clash-nyanpasu/pnpm-lock.yaml +++ b/clash-nyanpasu/pnpm-lock.yaml @@ -24,7 +24,7 @@ importers: devDependencies: '@commitlint/cli': specifier: 19.8.1 - version: 19.8.1(@types/node@24.3.1)(typescript@5.9.2) + version: 19.8.1(@types/node@24.5.2)(typescript@5.9.2) '@commitlint/config-conventional': specifier: 19.8.1 version: 19.8.1 @@ -47,8 +47,8 @@ importers: specifier: 4.17.12 version: 4.17.12 '@types/node': - specifier: 24.3.1 - version: 24.3.1 + specifier: 24.5.2 + version: 24.5.2 '@typescript-eslint/eslint-plugin': specifier: 8.44.1 version: 8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2))(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2) @@ -105,7 +105,7 @@ importers: version: 16.4.0 knip: specifier: 5.64.1 - version: 5.64.1(@types/node@24.3.1)(typescript@5.9.2) + version: 5.64.1(@types/node@24.5.2)(typescript@5.9.2) lint-staged: specifier: 16.2.1 version: 16.2.1 @@ -167,8 +167,8 @@ importers: specifier: 4.1.13 version: 4.1.13 tsx: - specifier: 4.20.5 - version: 4.20.5 + specifier: 4.20.6 + version: 4.20.6 typescript: specifier: 5.9.2 version: 5.9.2 @@ -207,8 +207,8 @@ importers: specifier: 4.17.12 version: 4.17.12 '@types/react': - specifier: 19.1.12 - version: 19.1.12 + specifier: 19.1.14 + version: 19.1.14 frontend/nyanpasu: dependencies: @@ -223,7 +223,7 @@ importers: version: 3.2.2(react@19.1.1) '@emotion/styled': specifier: 11.14.1 - version: 11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) + version: 11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1) '@juggle/resize-observer': specifier: 3.4.0 version: 3.4.0 @@ -232,16 +232,16 @@ importers: version: 0.3.0 '@mui/icons-material': specifier: 7.3.2 - version: 7.3.2(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) + version: 7.3.2(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.14)(react@19.1.1) '@mui/lab': specifier: 7.0.0-beta.17 - version: 7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@mui/material': specifier: 7.3.2 - version: 7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@mui/x-date-pickers': specifier: 8.12.0 - version: 8.12.0(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(dayjs@1.11.18)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 8.12.0(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(dayjs@1.11.18)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@nyanpasu/interface': specifier: workspace:^ version: link:../interface @@ -282,20 +282,20 @@ importers: specifier: 25.5.2 version: 25.5.2(typescript@5.9.2) jotai: - specifier: 2.14.0 - version: 2.14.0(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.1.12)(react@19.1.1) + specifier: 2.15.0 + version: 2.15.0(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.1.14)(react@19.1.1) json-schema: specifier: 0.4.0 version: 0.4.0 material-react-table: specifier: npm:@greenhat616/material-react-table@4.0.0 - version: '@greenhat616/material-react-table@4.0.0(306c2a7e09663f4b4ca18aa376f632d7)' + version: '@greenhat616/material-react-table@4.0.0(1271a7024e323b47c86660a86391d8f0)' monaco-editor: specifier: 0.52.2 version: 0.52.2 mui-color-input: specifier: 7.0.0 - version: 7.0.0(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 7.0.0(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react: specifier: 19.1.1 version: 19.1.1 @@ -310,13 +310,13 @@ importers: version: 1.6.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react-hook-form-mui: specifier: 8.0.0 - version: 8.0.0(a67848f6b3a393292e2dcb1a943d7b01) + version: 8.0.0(5034abb0a696b1128593edad6cde9ffd) react-i18next: specifier: 15.7.4 version: 15.7.4(i18next@25.5.2(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2) react-markdown: specifier: 10.1.0 - version: 10.1.0(@types/react@19.1.12)(react@19.1.1) + version: 10.1.0(@types/react@19.1.14)(react@19.1.1) react-split-grid: specifier: 1.0.4 version: 1.0.4(react@19.1.1) @@ -330,8 +330,8 @@ importers: specifier: 2.3.6 version: 2.3.6(react@19.1.1) virtua: - specifier: 0.43.4 - version: 0.43.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.5) + specifier: 0.43.5 + version: 0.43.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.5) vite-bundle-visualizer: specifier: 1.2.1 version: 1.2.1(rollup@4.46.2) @@ -344,7 +344,7 @@ importers: version: 11.13.5 '@emotion/react': specifier: 11.14.0 - version: 11.14.0(@types/react@19.1.12)(react@19.1.1) + version: 11.14.0(@types/react@19.1.14)(react@19.1.1) '@iconify/json': specifier: 2.2.388 version: 2.2.388 @@ -358,11 +358,11 @@ importers: specifier: 1.132.7 version: 1.132.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@tanstack/react-router-devtools': - specifier: 1.132.7 - version: 1.132.7(@tanstack/react-router@1.132.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@tanstack/router-core@1.132.7)(@types/node@24.3.1)(csstype@3.1.3)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass-embedded@1.93.2)(sass@1.93.2)(solid-js@1.9.5)(stylus@0.62.0)(terser@5.36.0)(tiny-invariant@1.3.3)(tsx@4.20.5)(yaml@2.8.1) + specifier: 1.132.11 + version: 1.132.11(@tanstack/react-router@1.132.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@tanstack/router-core@1.132.7)(@types/node@24.5.2)(csstype@3.1.3)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass-embedded@1.93.2)(sass@1.93.2)(solid-js@1.9.5)(stylus@0.62.0)(terser@5.36.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1) '@tanstack/router-plugin': specifier: 1.132.7 - version: 1.132.7(@tanstack/react-router@1.132.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1)) + version: 1.132.7(@tanstack/react-router@1.132.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1)) '@tauri-apps/plugin-clipboard-manager': specifier: 2.3.0 version: 2.3.0 @@ -388,23 +388,23 @@ importers: specifier: 2.9.0 version: 2.9.0 '@types/react': - specifier: 19.1.12 - version: 19.1.12 + specifier: 19.1.14 + version: 19.1.14 '@types/react-dom': specifier: 19.1.9 - version: 19.1.9(@types/react@19.1.12) + version: 19.1.9(@types/react@19.1.14) '@types/validator': specifier: 13.15.3 version: 13.15.3 '@vitejs/plugin-legacy': specifier: 7.2.1 - version: 7.2.1(terser@5.36.0)(vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1)) + version: 7.2.1(terser@5.36.0)(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1)) '@vitejs/plugin-react': specifier: 5.0.3 - version: 5.0.3(vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1)) + version: 5.0.3(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1)) '@vitejs/plugin-react-swc': specifier: 4.1.0 - version: 4.1.0(vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1)) + version: 4.1.0(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1)) change-case: specifier: 5.4.4 version: 5.4.4 @@ -443,19 +443,19 @@ importers: version: 13.15.15 vite: specifier: 7.1.7 - version: 7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1) + version: 7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1) vite-plugin-html: specifier: 3.2.2 - version: 3.2.2(vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1)) + version: 3.2.2(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1)) vite-plugin-sass-dts: specifier: 1.3.31 - version: 1.3.31(postcss@8.5.6)(prettier@3.6.2)(sass-embedded@1.93.2)(vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1)) + version: 1.3.31(postcss@8.5.6)(prettier@3.6.2)(sass-embedded@1.93.2)(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1)) vite-plugin-svgr: specifier: 4.5.0 - version: 4.5.0(rollup@4.46.2)(typescript@5.9.2)(vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1)) + version: 4.5.0(rollup@4.46.2)(typescript@5.9.2)(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1)) vite-tsconfig-paths: specifier: 5.1.4 - version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1)) + version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1)) zod: specifier: 4.1.11 version: 4.1.11 @@ -467,19 +467,19 @@ importers: version: 0.3.0 '@mui/icons-material': specifier: 7.3.2 - version: 7.3.2(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) + version: 7.3.2(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.14)(react@19.1.1) '@mui/lab': specifier: 7.0.0-beta.17 - version: 7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@mui/material': specifier: 7.3.2 - version: 7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@radix-ui/react-portal': specifier: 1.1.9 - version: 1.1.9(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.1.9(@types/react-dom@19.1.9(@types/react@19.1.14))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@radix-ui/react-scroll-area': specifier: 1.2.10 - version: 1.2.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.2.10(@types/react-dom@19.1.9(@types/react@19.1.14))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@tauri-apps/api': specifier: 2.8.0 version: 2.8.0 @@ -487,11 +487,11 @@ importers: specifier: 7.4.3 version: 7.4.3 '@types/react': - specifier: 19.1.12 - version: 19.1.12 + specifier: 19.1.14 + version: 19.1.14 '@vitejs/plugin-react': specifier: 5.0.3 - version: 5.0.3(vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1)) + version: 5.0.3(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1)) ahooks: specifier: 3.9.5 version: 3.9.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -521,14 +521,14 @@ importers: version: 4.1.13 vite: specifier: 7.1.7 - version: 7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1) + version: 7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1) vite-tsconfig-paths: specifier: 5.1.4 - version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1)) + version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1)) devDependencies: '@emotion/react': specifier: 11.14.0 - version: 11.14.0(@types/react@19.1.12)(react@19.1.1) + version: 11.14.0(@types/react@19.1.14)(react@19.1.1) '@types/d3-interpolate-path': specifier: 2.0.3 version: 2.0.3 @@ -549,7 +549,7 @@ importers: version: 5.2.0(typescript@5.9.2) vite-plugin-dts: specifier: 4.5.4 - version: 4.5.4(@types/node@24.3.1)(rollup@4.46.2)(typescript@5.9.2)(vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1)) + version: 4.5.4(@types/node@24.5.2)(rollup@4.46.2)(typescript@5.9.2)(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1)) scripts: dependencies: @@ -3042,8 +3042,8 @@ packages: peerDependencies: react: ^18 || ^19 - '@tanstack/react-router-devtools@1.132.7': - resolution: {integrity: sha512-Sx+Vx96seR/trFL5SXUEh8LxfqVkYGFQsTm2jAilMtAZKD0Mt7gZ2j4o14kpazevxUv7nUI7c+vdINGipyfEcA==} + '@tanstack/react-router-devtools@1.132.11': + resolution: {integrity: sha512-5UxJjyHxDBX2Sh8E5oN+YagfHiQ0bnhDx6Gguff2sCcMw/MgxaAcQ51EoQcAOwt36KAvh9CIp6VJiM9X6uhVRw==} engines: {node: '>=12'} peerDependencies: '@tanstack/react-router': ^1.132.7 @@ -3080,8 +3080,8 @@ packages: resolution: {integrity: sha512-HgarEo427V1rlpQtA1jIoK4/S/JYgM7/mU3TKO2d7/tyx0FRE9LP8hvUmlxz/Zi71lD6yaICHoCSVcOh0dGLbA==} engines: {node: '>=12'} - '@tanstack/router-devtools-core@1.132.7': - resolution: {integrity: sha512-pSu479zcw+e/cAv8PLQ7oIAnqQ5G5EjibYrj+fY6ShMZrRPBw09AudchrCbFBIF0YYAOY1narav7xHeqEJZpIA==} + '@tanstack/router-devtools-core@1.132.11': + resolution: {integrity: sha512-77cMrmVzLXVuSxTr6HTQSTVqzRtY4ITyDk4pZs2QI84dHujt0aMjHj9WnvqBe/0QW0nqgJBi4e8PmsVNXgUv7Q==} engines: {node: '>=12'} peerDependencies: '@tanstack/router-core': ^1.132.7 @@ -3441,8 +3441,8 @@ packages: '@types/node@16.18.108': resolution: {integrity: sha512-fj42LD82fSv6yN9C6Q4dzS+hujHj+pTv0IpRR3kI20fnYeS0ytBpjFO9OjmDowSPPt4lNKN46JLaKbCyP+BW2A==} - '@types/node@24.3.1': - resolution: {integrity: sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==} + '@types/node@24.5.2': + resolution: {integrity: sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -3466,8 +3466,8 @@ packages: peerDependencies: '@types/react': '*' - '@types/react@19.1.12': - resolution: {integrity: sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==} + '@types/react@19.1.14': + resolution: {integrity: sha512-ukd93VGzaNPMAUPy0gRDSC57UuQbnH9Kussp7HBjM06YFi9uZTFhOvMSO2OKqXm1rSgzOE+pVx1k1PYHGwlc8Q==} '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} @@ -5979,8 +5979,8 @@ packages: jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} - jotai@2.14.0: - resolution: {integrity: sha512-JQkNkTnqjk1BlSUjHfXi+pGG/573bVN104gp6CymhrWDseZGDReTNniWrLhJ+zXbM6pH+82+UNJ2vwYQUkQMWQ==} + jotai@2.15.0: + resolution: {integrity: sha512-nbp/6jN2Ftxgw0VwoVnOg0m5qYM1rVcfvij+MZx99Z5IK13eGve9FJoCwGv+17JvVthTjhSmNtT5e1coJnr6aw==} engines: {node: '>=12.20.0'} peerDependencies: '@babel/core': '>=7.0.0' @@ -8087,8 +8087,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.20.5: - resolution: {integrity: sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==} + tsx@4.20.6: + resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} engines: {node: '>=18.0.0'} hasBin: true @@ -8177,8 +8177,8 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@7.10.0: - resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} + undici-types@7.12.0: + resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==} undici@5.29.0: resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} @@ -8358,8 +8358,8 @@ packages: vfile@6.0.1: resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==} - virtua@0.43.4: - resolution: {integrity: sha512-zS9A2EC3KY9hRjG5NM7t0gPGqkcdOc7HWhuc9KDvKq3yz10sRup5L09axFyWHePr1ppWljpAHtr0BCCsM6tS5Q==} + virtua@0.43.5: + resolution: {integrity: sha512-Isu5UvvE+hs+1iTUfBgS4FakbAqgG8ALZP1/kLSyEASvHI4rANpWi5g+1BF8sUXVcWHStBniGVLZv1iX2i/qPw==} peerDependencies: react: '>=16.14.0' react-dom: '>=16.14.0' @@ -9729,11 +9729,11 @@ snapshots: '@bufbuild/protobuf@2.5.2': {} - '@commitlint/cli@19.8.1(@types/node@24.3.1)(typescript@5.9.2)': + '@commitlint/cli@19.8.1(@types/node@24.5.2)(typescript@5.9.2)': dependencies: '@commitlint/format': 19.8.1 '@commitlint/lint': 19.8.1 - '@commitlint/load': 19.8.1(@types/node@24.3.1)(typescript@5.9.2) + '@commitlint/load': 19.8.1(@types/node@24.5.2)(typescript@5.9.2) '@commitlint/read': 19.8.1 '@commitlint/types': 19.8.1 tinyexec: 1.0.1 @@ -9780,7 +9780,7 @@ snapshots: '@commitlint/rules': 19.8.1 '@commitlint/types': 19.8.1 - '@commitlint/load@19.8.1(@types/node@24.3.1)(typescript@5.9.2)': + '@commitlint/load@19.8.1(@types/node@24.5.2)(typescript@5.9.2)': dependencies: '@commitlint/config-validator': 19.8.1 '@commitlint/execute-rule': 19.8.1 @@ -9788,7 +9788,7 @@ snapshots: '@commitlint/types': 19.8.1 chalk: 5.4.1 cosmiconfig: 9.0.0(typescript@5.9.2) - cosmiconfig-typescript-loader: 6.1.0(@types/node@24.3.1)(cosmiconfig@9.0.0(typescript@5.9.2))(typescript@5.9.2) + cosmiconfig-typescript-loader: 6.1.0(@types/node@24.5.2)(cosmiconfig@9.0.0(typescript@5.9.2))(typescript@5.9.2) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -9965,7 +9965,7 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1)': + '@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1)': dependencies: '@babel/runtime': 7.26.0 '@emotion/babel-plugin': 11.13.5 @@ -9977,7 +9977,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 19.1.1 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.1.14 transitivePeerDependencies: - supports-color @@ -9991,18 +9991,18 @@ snapshots: '@emotion/sheet@1.4.0': {} - '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1)': + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1)': dependencies: '@babel/runtime': 7.27.6 '@emotion/babel-plugin': 11.13.5 '@emotion/is-prop-valid': 1.3.0 - '@emotion/react': 11.14.0(@types/react@19.1.12)(react@19.1.1) + '@emotion/react': 11.14.0(@types/react@19.1.14)(react@19.1.1) '@emotion/serialize': 1.3.3 '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.1) '@emotion/utils': 1.4.2 react: 19.1.1 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.1.14 transitivePeerDependencies: - supports-color @@ -10154,13 +10154,13 @@ snapshots: '@fastify/busboy@2.1.1': {} - '@greenhat616/material-react-table@4.0.0(306c2a7e09663f4b4ca18aa376f632d7)': + '@greenhat616/material-react-table@4.0.0(1271a7024e323b47c86660a86391d8f0)': dependencies: - '@emotion/react': 11.14.0(@types/react@19.1.12)(react@19.1.1) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) - '@mui/icons-material': 7.3.2(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) - '@mui/material': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@mui/x-date-pickers': 8.12.0(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(dayjs@1.11.18)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@emotion/react': 11.14.0(@types/react@19.1.14)(react@19.1.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1) + '@mui/icons-material': 7.3.2(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.14)(react@19.1.1) + '@mui/material': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@mui/x-date-pickers': 8.12.0(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(dayjs@1.11.18)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@tanstack/match-sorter-utils': 8.19.4 '@tanstack/react-table': 8.21.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@tanstack/react-virtual': 3.13.9(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -10254,23 +10254,23 @@ snapshots: '@material/material-color-utilities@0.3.0': {} - '@microsoft/api-extractor-model@7.30.3(@types/node@24.3.1)': + '@microsoft/api-extractor-model@7.30.3(@types/node@24.5.2)': dependencies: '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.11.0(@types/node@24.3.1) + '@rushstack/node-core-library': 5.11.0(@types/node@24.5.2) transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor@7.51.0(@types/node@24.3.1)': + '@microsoft/api-extractor@7.51.0(@types/node@24.5.2)': dependencies: - '@microsoft/api-extractor-model': 7.30.3(@types/node@24.3.1) + '@microsoft/api-extractor-model': 7.30.3(@types/node@24.5.2) '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.11.0(@types/node@24.3.1) + '@rushstack/node-core-library': 5.11.0(@types/node@24.5.2) '@rushstack/rig-package': 0.5.3 - '@rushstack/terminal': 0.15.0(@types/node@24.3.1) - '@rushstack/ts-command-line': 4.23.5(@types/node@24.3.1) + '@rushstack/terminal': 0.15.0(@types/node@24.5.2) + '@rushstack/ts-command-line': 4.23.5(@types/node@24.5.2) lodash: 4.17.21 minimatch: 3.0.8 resolve: 1.22.8 @@ -10302,39 +10302,39 @@ snapshots: '@mui/core-downloads-tracker@7.3.2': {} - '@mui/icons-material@7.3.2(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.12)(react@19.1.1)': + '@mui/icons-material@7.3.2(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.14)(react@19.1.1)': dependencies: '@babel/runtime': 7.28.3 - '@mui/material': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@mui/material': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react: 19.1.1 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.1.14 - '@mui/lab@7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@mui/lab@7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@babel/runtime': 7.28.3 - '@mui/material': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@mui/system': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) - '@mui/types': 7.4.6(@types/react@19.1.12) - '@mui/utils': 7.3.2(@types/react@19.1.12)(react@19.1.1) + '@mui/material': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@mui/system': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1) + '@mui/types': 7.4.6(@types/react@19.1.14) + '@mui/utils': 7.3.2(@types/react@19.1.14)(react@19.1.1) clsx: 2.1.1 prop-types: 15.8.1 react: 19.1.1 react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.1.12)(react@19.1.1) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) - '@types/react': 19.1.12 + '@emotion/react': 11.14.0(@types/react@19.1.14)(react@19.1.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1) + '@types/react': 19.1.14 - '@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@babel/runtime': 7.28.3 '@mui/core-downloads-tracker': 7.3.2 - '@mui/system': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) - '@mui/types': 7.4.6(@types/react@19.1.12) - '@mui/utils': 7.3.2(@types/react@19.1.12)(react@19.1.1) + '@mui/system': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1) + '@mui/types': 7.4.6(@types/react@19.1.14) + '@mui/utils': 7.3.2(@types/react@19.1.14)(react@19.1.1) '@popperjs/core': 2.11.8 - '@types/react-transition-group': 4.4.12(@types/react@19.1.12) + '@types/react-transition-group': 4.4.12(@types/react@19.1.14) clsx: 2.1.1 csstype: 3.1.3 prop-types: 15.8.1 @@ -10343,20 +10343,20 @@ snapshots: react-is: 19.1.1 react-transition-group: 4.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.1.12)(react@19.1.1) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) - '@types/react': 19.1.12 + '@emotion/react': 11.14.0(@types/react@19.1.14)(react@19.1.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1) + '@types/react': 19.1.14 - '@mui/private-theming@7.3.2(@types/react@19.1.12)(react@19.1.1)': + '@mui/private-theming@7.3.2(@types/react@19.1.14)(react@19.1.1)': dependencies: '@babel/runtime': 7.28.3 - '@mui/utils': 7.3.2(@types/react@19.1.12)(react@19.1.1) + '@mui/utils': 7.3.2(@types/react@19.1.14)(react@19.1.1) prop-types: 15.8.1 react: 19.1.1 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.1.14 - '@mui/styled-engine@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1)': + '@mui/styled-engine@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(react@19.1.1)': dependencies: '@babel/runtime': 7.28.3 '@emotion/cache': 11.14.0 @@ -10366,67 +10366,67 @@ snapshots: prop-types: 15.8.1 react: 19.1.1 optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.1.12)(react@19.1.1) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) + '@emotion/react': 11.14.0(@types/react@19.1.14)(react@19.1.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1) - '@mui/system@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1)': + '@mui/system@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1)': dependencies: '@babel/runtime': 7.28.3 - '@mui/private-theming': 7.3.2(@types/react@19.1.12)(react@19.1.1) - '@mui/styled-engine': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1) - '@mui/types': 7.4.6(@types/react@19.1.12) - '@mui/utils': 7.3.2(@types/react@19.1.12)(react@19.1.1) + '@mui/private-theming': 7.3.2(@types/react@19.1.14)(react@19.1.1) + '@mui/styled-engine': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(react@19.1.1) + '@mui/types': 7.4.6(@types/react@19.1.14) + '@mui/utils': 7.3.2(@types/react@19.1.14)(react@19.1.1) clsx: 2.1.1 csstype: 3.1.3 prop-types: 15.8.1 react: 19.1.1 optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.1.12)(react@19.1.1) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) - '@types/react': 19.1.12 + '@emotion/react': 11.14.0(@types/react@19.1.14)(react@19.1.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1) + '@types/react': 19.1.14 - '@mui/types@7.4.6(@types/react@19.1.12)': + '@mui/types@7.4.6(@types/react@19.1.14)': dependencies: '@babel/runtime': 7.28.3 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.1.14 - '@mui/utils@7.3.2(@types/react@19.1.12)(react@19.1.1)': + '@mui/utils@7.3.2(@types/react@19.1.14)(react@19.1.1)': dependencies: '@babel/runtime': 7.28.3 - '@mui/types': 7.4.6(@types/react@19.1.12) + '@mui/types': 7.4.6(@types/react@19.1.14) '@types/prop-types': 15.7.15 clsx: 2.1.1 prop-types: 15.8.1 react: 19.1.1 react-is: 19.1.1 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.1.14 - '@mui/x-date-pickers@8.12.0(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(dayjs@1.11.18)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@mui/x-date-pickers@8.12.0(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(dayjs@1.11.18)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@babel/runtime': 7.28.3 - '@mui/material': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@mui/system': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) - '@mui/utils': 7.3.2(@types/react@19.1.12)(react@19.1.1) - '@mui/x-internals': 8.12.0(@types/react@19.1.12)(react@19.1.1) - '@types/react-transition-group': 4.4.12(@types/react@19.1.12) + '@mui/material': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@mui/system': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1) + '@mui/utils': 7.3.2(@types/react@19.1.14)(react@19.1.1) + '@mui/x-internals': 8.12.0(@types/react@19.1.14)(react@19.1.1) + '@types/react-transition-group': 4.4.12(@types/react@19.1.14) clsx: 2.1.1 prop-types: 15.8.1 react: 19.1.1 react-dom: 19.1.1(react@19.1.1) react-transition-group: 4.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.1.12)(react@19.1.1) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) + '@emotion/react': 11.14.0(@types/react@19.1.14)(react@19.1.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1) dayjs: 1.11.18 transitivePeerDependencies: - '@types/react' - '@mui/x-internals@8.12.0(@types/react@19.1.12)(react@19.1.1)': + '@mui/x-internals@8.12.0(@types/react@19.1.14)(react@19.1.1)': dependencies: '@babel/runtime': 7.28.3 - '@mui/utils': 7.3.2(@types/react@19.1.12)(react@19.1.1) + '@mui/utils': 7.3.2(@types/react@19.1.14)(react@19.1.1) react: 19.1.1 reselect: 5.1.1 use-sync-external-store: 1.5.0(react@19.1.1) @@ -10843,88 +10843,88 @@ snapshots: '@radix-ui/primitive@1.1.3': {} - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.14)(react@19.1.1)': dependencies: react: 19.1.1 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.1.14 - '@radix-ui/react-context@1.1.2(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-context@1.1.2(@types/react@19.1.14)(react@19.1.1)': dependencies: react: 19.1.1 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.1.14 - '@radix-ui/react-direction@1.1.1(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-direction@1.1.1(@types/react@19.1.14)(react@19.1.1)': dependencies: react: 19.1.1 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.1.14 - '@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.9(@types/react@19.1.14))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.14))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.14)(react@19.1.1) react: 19.1.1 react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.1.14 + '@types/react-dom': 19.1.9(@types/react@19.1.14) - '@radix-ui/react-presence@1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.1.9(@types/react@19.1.14))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.14)(react@19.1.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.14)(react@19.1.1) react: 19.1.1 react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.1.14 + '@types/react-dom': 19.1.9(@types/react@19.1.14) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.9(@types/react@19.1.14))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.14)(react@19.1.1) react: 19.1.1 react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.1.14 + '@types/react-dom': 19.1.9(@types/react@19.1.14) - '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.1.9(@types/react@19.1.14))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.14)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.14)(react@19.1.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.14)(react@19.1.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.14))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.14))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.14)(react@19.1.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.14)(react@19.1.1) react: 19.1.1 react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.1.14 + '@types/react-dom': 19.1.9(@types/react@19.1.14) - '@radix-ui/react-slot@1.2.3(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-slot@1.2.3(@types/react@19.1.14)(react@19.1.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.14)(react@19.1.1) react: 19.1.1 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.1.14 - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.14)(react@19.1.1)': dependencies: react: 19.1.1 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.1.14 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.14)(react@19.1.1)': dependencies: react: 19.1.1 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.1.14 '@rolldown/pluginutils@1.0.0-beta.35': {} @@ -11011,7 +11011,7 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@rushstack/node-core-library@5.11.0(@types/node@24.3.1)': + '@rushstack/node-core-library@5.11.0(@types/node@24.5.2)': dependencies: ajv: 8.13.0 ajv-draft-04: 1.0.0(ajv@8.13.0) @@ -11022,23 +11022,23 @@ snapshots: resolve: 1.22.8 semver: 7.5.4 optionalDependencies: - '@types/node': 24.3.1 + '@types/node': 24.5.2 '@rushstack/rig-package@0.5.3': dependencies: resolve: 1.22.8 strip-json-comments: 3.1.1 - '@rushstack/terminal@0.15.0(@types/node@24.3.1)': + '@rushstack/terminal@0.15.0(@types/node@24.5.2)': dependencies: - '@rushstack/node-core-library': 5.11.0(@types/node@24.3.1) + '@rushstack/node-core-library': 5.11.0(@types/node@24.5.2) supports-color: 8.1.1 optionalDependencies: - '@types/node': 24.3.1 + '@types/node': 24.5.2 - '@rushstack/ts-command-line@4.23.5(@types/node@24.3.1)': + '@rushstack/ts-command-line@4.23.5(@types/node@24.5.2)': dependencies: - '@rushstack/terminal': 0.15.0(@types/node@24.3.1) + '@rushstack/terminal': 0.15.0(@types/node@24.5.2) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 @@ -11305,13 +11305,13 @@ snapshots: '@tanstack/query-core': 5.90.2 react: 19.1.1 - '@tanstack/react-router-devtools@1.132.7(@tanstack/react-router@1.132.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@tanstack/router-core@1.132.7)(@types/node@24.3.1)(csstype@3.1.3)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass-embedded@1.93.2)(sass@1.93.2)(solid-js@1.9.5)(stylus@0.62.0)(terser@5.36.0)(tiny-invariant@1.3.3)(tsx@4.20.5)(yaml@2.8.1)': + '@tanstack/react-router-devtools@1.132.11(@tanstack/react-router@1.132.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@tanstack/router-core@1.132.7)(@types/node@24.5.2)(csstype@3.1.3)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass-embedded@1.93.2)(sass@1.93.2)(solid-js@1.9.5)(stylus@0.62.0)(terser@5.36.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1)': dependencies: '@tanstack/react-router': 1.132.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@tanstack/router-devtools-core': 1.132.7(@tanstack/router-core@1.132.7)(@types/node@24.3.1)(csstype@3.1.3)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(solid-js@1.9.5)(stylus@0.62.0)(terser@5.36.0)(tiny-invariant@1.3.3)(tsx@4.20.5)(yaml@2.8.1) + '@tanstack/router-devtools-core': 1.132.11(@tanstack/router-core@1.132.7)(@types/node@24.5.2)(csstype@3.1.3)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(solid-js@1.9.5)(stylus@0.62.0)(terser@5.36.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1) react: 19.1.1 react-dom: 19.1.1(react@19.1.1) - vite: 7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1) + vite: 7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - '@tanstack/router-core' - '@types/node' @@ -11369,14 +11369,14 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/router-devtools-core@1.132.7(@tanstack/router-core@1.132.7)(@types/node@24.3.1)(csstype@3.1.3)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(solid-js@1.9.5)(stylus@0.62.0)(terser@5.36.0)(tiny-invariant@1.3.3)(tsx@4.20.5)(yaml@2.8.1)': + '@tanstack/router-devtools-core@1.132.11(@tanstack/router-core@1.132.7)(@types/node@24.5.2)(csstype@3.1.3)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(solid-js@1.9.5)(stylus@0.62.0)(terser@5.36.0)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.1)': dependencies: '@tanstack/router-core': 1.132.7 clsx: 2.1.1 goober: 2.1.16(csstype@3.1.3) solid-js: 1.9.5 tiny-invariant: 1.3.3 - vite: 7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1) + vite: 7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1) optionalDependencies: csstype: 3.1.3 transitivePeerDependencies: @@ -11400,12 +11400,12 @@ snapshots: prettier: 3.6.2 recast: 0.23.11 source-map: 0.7.4 - tsx: 4.20.5 + tsx: 4.20.6 zod: 3.25.76 transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.132.7(@tanstack/react-router@1.132.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1))': + '@tanstack/router-plugin@1.132.7(@tanstack/react-router@1.132.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) @@ -11423,7 +11423,7 @@ snapshots: zod: 3.25.76 optionalDependencies: '@tanstack/react-router': 1.132.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - vite: 7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1) + vite: 7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -11565,7 +11565,7 @@ snapshots: '@types/adm-zip@0.5.7': dependencies: - '@types/node': 24.3.1 + '@types/node': 24.5.2 '@types/argparse@1.0.38': {} @@ -11596,12 +11596,12 @@ snapshots: dependencies: '@types/http-cache-semantics': 4.0.4 '@types/keyv': 3.1.4 - '@types/node': 24.3.1 + '@types/node': 24.5.2 '@types/responselike': 1.0.3 '@types/conventional-commits-parser@5.0.0': dependencies: - '@types/node': 24.3.1 + '@types/node': 24.5.2 '@types/d3-array@3.2.1': {} @@ -11737,7 +11737,7 @@ snapshots: '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 - '@types/node': 24.3.1 + '@types/node': 24.5.2 '@types/geojson@7946.0.14': {} @@ -11757,11 +11757,11 @@ snapshots: '@types/jsonfile@6.1.4': dependencies: - '@types/node': 24.3.1 + '@types/node': 24.5.2 '@types/keyv@3.1.4': dependencies: - '@types/node': 24.3.1 + '@types/node': 24.5.2 '@types/lodash-es@4.17.12': dependencies: @@ -11777,9 +11777,9 @@ snapshots: '@types/node@16.18.108': {} - '@types/node@24.3.1': + '@types/node@24.5.2': dependencies: - undici-types: 7.10.0 + undici-types: 7.12.0 '@types/parse-json@4.0.2': {} @@ -11793,21 +11793,21 @@ snapshots: '@types/prop-types@15.7.15': {} - '@types/react-dom@19.1.9(@types/react@19.1.12)': + '@types/react-dom@19.1.9(@types/react@19.1.14)': dependencies: - '@types/react': 19.1.12 + '@types/react': 19.1.14 - '@types/react-transition-group@4.4.12(@types/react@19.1.12)': + '@types/react-transition-group@4.4.12(@types/react@19.1.14)': dependencies: - '@types/react': 19.1.12 + '@types/react': 19.1.14 - '@types/react@19.1.12': + '@types/react@19.1.14': dependencies: csstype: 3.1.3 '@types/responselike@1.0.3': dependencies: - '@types/node': 24.3.1 + '@types/node': 24.5.2 '@types/semver@7.7.1': {} @@ -11825,7 +11825,7 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 24.3.1 + '@types/node': 24.5.2 optional: true '@typescript-eslint/eslint-plugin@8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2))(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2)': @@ -12040,7 +12040,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.10.1': optional: true - '@vitejs/plugin-legacy@7.2.1(terser@5.36.0)(vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1))': + '@vitejs/plugin-legacy@7.2.1(terser@5.36.0)(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.0 '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.0) @@ -12055,19 +12055,19 @@ snapshots: regenerator-runtime: 0.14.1 systemjs: 6.15.1 terser: 5.36.0 - vite: 7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1) + vite: 7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react-swc@4.1.0(vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1))': + '@vitejs/plugin-react-swc@4.1.0(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.35 '@swc/core': 1.13.5 - vite: 7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1) + vite: 7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@5.0.3(vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1))': + '@vitejs/plugin-react@5.0.3(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) @@ -12075,7 +12075,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.35 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1) + vite: 7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -12752,9 +12752,9 @@ snapshots: core-js@3.45.1: {} - cosmiconfig-typescript-loader@6.1.0(@types/node@24.3.1)(cosmiconfig@9.0.0(typescript@5.9.2))(typescript@5.9.2): + cosmiconfig-typescript-loader@6.1.0(@types/node@24.5.2)(cosmiconfig@9.0.0(typescript@5.9.2))(typescript@5.9.2): dependencies: - '@types/node': 24.3.1 + '@types/node': 24.5.2 cosmiconfig: 9.0.0(typescript@5.9.2) jiti: 2.5.1 typescript: 5.9.2 @@ -14722,11 +14722,11 @@ snapshots: jju@1.4.0: {} - jotai@2.14.0(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.1.12)(react@19.1.1): + jotai@2.15.0(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.1.14)(react@19.1.1): optionalDependencies: '@babel/core': 7.28.4 '@babel/template': 7.27.2 - '@types/react': 19.1.12 + '@types/react': 19.1.14 react: 19.1.1 js-cookie@2.2.1: {} @@ -14804,10 +14804,10 @@ snapshots: kind-of@6.0.3: {} - knip@5.64.1(@types/node@24.3.1)(typescript@5.9.2): + knip@5.64.1(@types/node@24.5.2)(typescript@5.9.2): dependencies: '@nodelib/fs.walk': 1.2.8 - '@types/node': 24.3.1 + '@types/node': 24.5.2 fast-glob: 3.3.3 formatly: 0.3.0 jiti: 2.6.0 @@ -15375,16 +15375,16 @@ snapshots: muggle-string@0.4.1: {} - mui-color-input@7.0.0(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + mui-color-input@7.0.0(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: '@ctrl/tinycolor': 4.1.0 - '@emotion/react': 11.14.0(@types/react@19.1.12)(react@19.1.1) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) - '@mui/material': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@emotion/react': 11.14.0(@types/react@19.1.14)(react@19.1.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1) + '@mui/material': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react: 19.1.1 react-dom: 19.1.1(react@19.1.1) optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.1.14 nano-css@5.6.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: @@ -15977,14 +15977,14 @@ snapshots: react: 19.1.1 react-dom: 19.1.1(react@19.1.1) - react-hook-form-mui@8.0.0(a67848f6b3a393292e2dcb1a943d7b01): + react-hook-form-mui@8.0.0(5034abb0a696b1128593edad6cde9ffd): dependencies: - '@mui/material': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@mui/material': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react: 19.1.1 react-hook-form: 7.52.1(react@19.1.1) optionalDependencies: - '@mui/icons-material': 7.3.2(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) - '@mui/x-date-pickers': 8.12.0(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.2(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(dayjs@1.11.18)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@mui/icons-material': 7.3.2(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.14)(react@19.1.1) + '@mui/x-date-pickers': 8.12.0(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@mui/material@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(dayjs@1.11.18)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react-hook-form@7.52.1(react@19.1.1): dependencies: @@ -16004,11 +16004,11 @@ snapshots: react-is@19.1.1: {} - react-markdown@10.1.0(@types/react@19.1.12)(react@19.1.1): + react-markdown@10.1.0(@types/react@19.1.14)(react@19.1.1): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.3 - '@types/react': 19.1.12 + '@types/react': 19.1.14 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.0 html-url-attributes: 3.0.0 @@ -17035,7 +17035,7 @@ snapshots: tslib@2.8.1: {} - tsx@4.20.5: + tsx@4.20.6: dependencies: esbuild: 0.25.0 get-tsconfig: 4.10.1 @@ -17179,7 +17179,7 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@7.10.0: {} + undici-types@7.12.0: {} undici@5.29.0: dependencies: @@ -17392,7 +17392,7 @@ snapshots: unist-util-stringify-position: 4.0.0 vfile-message: 4.0.2 - virtua@0.43.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.5): + virtua@0.43.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.5): optionalDependencies: react: 19.1.1 react-dom: 19.1.1(react@19.1.1) @@ -17408,9 +17408,9 @@ snapshots: - rollup - supports-color - vite-plugin-dts@4.5.4(@types/node@24.3.1)(rollup@4.46.2)(typescript@5.9.2)(vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1)): + vite-plugin-dts@4.5.4(@types/node@24.5.2)(rollup@4.46.2)(typescript@5.9.2)(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: - '@microsoft/api-extractor': 7.51.0(@types/node@24.3.1) + '@microsoft/api-extractor': 7.51.0(@types/node@24.5.2) '@rollup/pluginutils': 5.1.4(rollup@4.46.2) '@volar/typescript': 2.4.11 '@vue/language-core': 2.2.0(typescript@5.9.2) @@ -17421,13 +17421,13 @@ snapshots: magic-string: 0.30.17 typescript: 5.9.2 optionalDependencies: - vite: 7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1) + vite: 7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite-plugin-html@3.2.2(vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1)): + vite-plugin-html@3.2.2(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: '@rollup/pluginutils': 4.2.1 colorette: 2.0.20 @@ -17441,39 +17441,39 @@ snapshots: html-minifier-terser: 6.1.0 node-html-parser: 5.4.2 pathe: 0.2.0 - vite: 7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1) + vite: 7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1) - vite-plugin-sass-dts@1.3.31(postcss@8.5.6)(prettier@3.6.2)(sass-embedded@1.93.2)(vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1)): + vite-plugin-sass-dts@1.3.31(postcss@8.5.6)(prettier@3.6.2)(sass-embedded@1.93.2)(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: postcss: 8.5.6 postcss-js: 4.0.1(postcss@8.5.6) prettier: 3.6.2 sass-embedded: 1.93.2 - vite: 7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1) + vite: 7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1) - vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.2)(vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1)): + vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.2)(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: '@rollup/pluginutils': 5.2.0(rollup@4.46.2) '@svgr/core': 8.1.0(typescript@5.9.2) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.2)) - vite: 7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1) + vite: 7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - rollup - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1)): + vite-tsconfig-paths@5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: debug: 4.3.7 globrex: 0.1.2 tsconfck: 3.0.3(typescript@5.9.2) optionalDependencies: - vite: 7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1) + vite: 7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - supports-color - typescript - vite@7.1.7(@types/node@24.3.1)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.5)(yaml@2.8.1): + vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(less@4.2.0)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.36.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.0 fdir: 6.5.0(picomatch@4.0.3) @@ -17482,7 +17482,7 @@ snapshots: rollup: 4.46.2 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 24.3.1 + '@types/node': 24.5.2 fsevents: 2.3.3 jiti: 2.6.0 less: 4.2.0 @@ -17491,7 +17491,7 @@ snapshots: sass-embedded: 1.93.2 stylus: 0.62.0 terser: 5.36.0 - tsx: 4.20.5 + tsx: 4.20.6 yaml: 2.8.1 void-elements@3.1.0: {} diff --git a/lede/include/kernel-6.1 b/lede/include/kernel-6.1 index dbd08753a4..bc14ef8341 100644 --- a/lede/include/kernel-6.1 +++ b/lede/include/kernel-6.1 @@ -1,2 +1,2 @@ -LINUX_VERSION-6.1 = .153 -LINUX_KERNEL_HASH-6.1.153 = 76ebbde05899ff712856ae9b06413d71ab7162771e7bc6df4d4ab335e1fc9e48 +LINUX_VERSION-6.1 = .154 +LINUX_KERNEL_HASH-6.1.154 = 6f1aba92c3d0d21299eebb73161a2056c7c78c6ee77462a7cb705bd8cca8b198 diff --git a/lede/include/kernel-6.12 b/lede/include/kernel-6.12 index dd7f683f1a..438cd31bb8 100644 --- a/lede/include/kernel-6.12 +++ b/lede/include/kernel-6.12 @@ -1,2 +1,2 @@ -LINUX_VERSION-6.12 = .48 -LINUX_KERNEL_HASH-6.12.48 = 5bf9eb676751bf48978e38363c772298b41a75336d5038ed6d37012399471db2 +LINUX_VERSION-6.12 = .49 +LINUX_KERNEL_HASH-6.12.49 = 234621e146dacce2241049555d550e4f7a6bde67ccd7ef232d47ac8145425526 diff --git a/lede/include/kernel-6.6 b/lede/include/kernel-6.6 index ecfe1f667e..b1eaea13b2 100644 --- a/lede/include/kernel-6.6 +++ b/lede/include/kernel-6.6 @@ -1,2 +1,2 @@ -LINUX_VERSION-6.6 = .107 -LINUX_KERNEL_HASH-6.6.107 = c9b73017d3c7867b9c69a542e5318620cf2b14ed23d52f6aeaaee8aded9ee447 +LINUX_VERSION-6.6 = .108 +LINUX_KERNEL_HASH-6.6.108 = 601cd332aa695d16607b353feff369b4a0b36d111be077e8af54bdd41e1968a6 diff --git a/lede/include/version.mk b/lede/include/version.mk index 3896ad0357..abd7bce7f3 100644 --- a/lede/include/version.mk +++ b/lede/include/version.mk @@ -23,13 +23,13 @@ PKG_CONFIG_DEPENDS += \ sanitize = $(call tolower,$(subst _,-,$(subst $(space),-,$(1)))) VERSION_NUMBER:=$(call qstrip,$(CONFIG_VERSION_NUMBER)) -VERSION_NUMBER:=$(if $(VERSION_NUMBER),$(VERSION_NUMBER),24.10.2) +VERSION_NUMBER:=$(if $(VERSION_NUMBER),$(VERSION_NUMBER),24.10.3) VERSION_CODE:=$(call qstrip,$(CONFIG_VERSION_CODE)) VERSION_CODE:=$(if $(VERSION_CODE),$(VERSION_CODE),$(REVISION)) VERSION_REPO:=$(call qstrip,$(CONFIG_VERSION_REPO)) -VERSION_REPO:=$(if $(VERSION_REPO),$(VERSION_REPO),https://downloads.openwrt.org/releases/24.10.2) +VERSION_REPO:=$(if $(VERSION_REPO),$(VERSION_REPO),https://downloads.openwrt.org/releases/24.10.3) VERSION_DIST:=$(call qstrip,$(CONFIG_VERSION_DIST)) VERSION_DIST:=$(if $(VERSION_DIST),$(VERSION_DIST),OpenWrt) diff --git a/lede/package/base-files/image-config.in b/lede/package/base-files/image-config.in index 4d5da196ae..73c62b349a 100644 --- a/lede/package/base-files/image-config.in +++ b/lede/package/base-files/image-config.in @@ -190,7 +190,7 @@ if VERSIONOPT config VERSION_REPO string prompt "Release repository" - default "https://downloads.openwrt.org/releases/24.10.2" + default "https://downloads.openwrt.org/releases/24.10.3" help This is the repository address embedded in the image, it defaults to the trunk snapshot repo; the url may contain the following placeholders: diff --git a/lede/target/linux/phytium/patches-5.10/001-add-phytium-support.patch b/lede/target/linux/phytium/patches-5.10/001-add-phytium-support.patch index 4de812ae3a..b28f249dc1 100644 --- a/lede/target/linux/phytium/patches-5.10/001-add-phytium-support.patch +++ b/lede/target/linux/phytium/patches-5.10/001-add-phytium-support.patch @@ -322,14 +322,13 @@ } --- a/arch/arm64/include/asm/cpucaps.h +++ b/arch/arm64/include/asm/cpucaps.h -@@ -69,7 +69,7 @@ - #define ARM64_SPECTRE_BHB 59 - #define ARM64_WORKAROUND_2457168 60 +@@ -71,6 +71,7 @@ #define ARM64_WORKAROUND_1742098 61 -- --#define ARM64_NCAPS 62 -+#define ARM64_HAS_ECV 62 -+#define ARM64_NCAPS 63 + #define ARM64_WORKAROUND_SPECULATIVE_SSBS 62 + +-#define ARM64_NCAPS 63 ++#define ARM64_HAS_ECV 63 ++#define ARM64_NCAPS 64 #endif /* __ASM_CPUCAPS_H */ --- a/arch/arm64/include/asm/esr.h @@ -2348,9 +2347,9 @@ struct clk *tsu_clk; struct net_device *dev; + struct ncsi_dev *ndev; + /* Protects hw_stats and ethtool_stats */ + spinlock_t stats_lock; union { - struct macb_stats macb; - struct gem_stats gem; @@ -1201,7 +1299,13 @@ struct macb { struct mii_bus *mii_bus; struct phylink *phylink; diff --git a/nodepass/README.md b/nodepass/README.md index 5efb44fc71..b2f1bb5c3a 100644 --- a/nodepass/README.md +++ b/nodepass/README.md @@ -1,5 +1,5 @@
- nodepass + [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go#networking) [![GitHub release](https://img.shields.io/github/v/release/yosebyte/nodepass)](https://github.com/yosebyte/nodepass/releases) @@ -10,6 +10,8 @@ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/yosebyte/nodepass) ![GitHub last commit](https://img.shields.io/github/last-commit/yosebyte/nodepass) + + English | [简体中文](README_zh.md)
@@ -98,6 +100,8 @@ The [NodePassProject](https://github.com/NodePassProject) organization develops - **[npsh](https://github.com/NodePassProject/npsh)**: A collection of one-click scripts that provide simple deployment for API or Dashboard with flexible configuration and management. +- **[NodePass-ApplePlatforms](https://github.com/NodePassProject/NodePass-ApplePlatforms)**: An iOS/macOS application that offers a native experience for Apple users. + - **[nodepass-core](https://github.com/NodePassProject/nodepass-core)**: Development branch, featuring previews of new functionalities and performance optimizations, suitable for advanced users and developers. ## 💬 Discussion diff --git a/nodepass/README_zh.md b/nodepass/README_zh.md index fb709cae75..f264de7c30 100644 --- a/nodepass/README_zh.md +++ b/nodepass/README_zh.md @@ -10,6 +10,8 @@ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/yosebyte/nodepass) ![GitHub last commit](https://img.shields.io/github/last-commit/yosebyte/nodepass) + + [English](README.md) | 简体中文 @@ -98,6 +100,8 @@ nodepass "master://:10101/api?log=debug&tls=1" - **[npsh](https://github.com/NodePassProject/npsh)**: 简单易用的 NodePass 一键脚本合集,包括 API 主控、Dash 面板的安装部署、灵活配置和辅助管理。 +- **[NodePass-ApplePlatforms](https://github.com/NodePassProject/NodePass-ApplePlatforms)**: 一款为 Apple 用户提供原生体验的 iOS/macOS 应用程序。 + - **[nodepass-core](https://github.com/NodePassProject/nodepass-core)**: 开发分支,包含新功能预览和性能优化测试,适合高级用户和开发者。 ## 💬 讨论 diff --git a/nodepass/cmd/nodepass/core.go b/nodepass/cmd/nodepass/core.go index 68e8197346..830484ca5d 100644 --- a/nodepass/cmd/nodepass/core.go +++ b/nodepass/cmd/nodepass/core.go @@ -2,37 +2,80 @@ package main import ( "crypto/tls" + "fmt" "net/url" + "os" + "runtime" "time" "github.com/NodePassProject/cert" + "github.com/NodePassProject/logs" "github.com/yosebyte/nodepass/internal" ) -// coreDispatch 根据URL方案分派到不同的运行模式 -func coreDispatch(parsedURL *url.URL) { - var core interface{ Run() } +// start 启动核心逻辑 +func start(args []string) error { + if len(args) != 2 { + return fmt.Errorf("start: empty URL command") + } - switch scheme := parsedURL.Scheme; scheme { - case "server", "master": - tlsCode, tlsConfig := getTLSProtocol(parsedURL) - if scheme == "server" { - core = internal.NewServer(parsedURL, tlsCode, tlsConfig, logger) - } else { - core = internal.NewMaster(parsedURL, tlsCode, tlsConfig, logger, version) - } - case "client": - core = internal.NewClient(parsedURL, logger) - default: - logger.Error("Unknown core: %v", scheme) - getExitInfo() + parsedURL, err := url.Parse(args[1]) + if err != nil { + return fmt.Errorf("start: parse URL failed: %v", err) + } + + logger := initLogger(parsedURL.Query().Get("log")) + + core, err := createCore(parsedURL, logger) + if err != nil { + return fmt.Errorf("start: create core failed: %v", err) } core.Run() + return nil +} + +// initLogger 初始化日志记录器 +func initLogger(level string) *logs.Logger { + logger := logs.NewLogger(logs.Info, true) + switch level { + case "none": + logger.SetLogLevel(logs.None) + case "debug": + logger.SetLogLevel(logs.Debug) + logger.Debug("Init log level: DEBUG") + case "warn": + logger.SetLogLevel(logs.Warn) + logger.Warn("Init log level: WARN") + case "error": + logger.SetLogLevel(logs.Error) + logger.Error("Init log level: ERROR") + case "event": + logger.SetLogLevel(logs.Event) + logger.Event("Init log level: EVENT") + default: + } + return logger +} + +// createCore 创建核心 +func createCore(parsedURL *url.URL, logger *logs.Logger) (interface{ Run() }, error) { + switch parsedURL.Scheme { + case "server": + tlsCode, tlsConfig := getTLSProtocol(parsedURL, logger) + return internal.NewServer(parsedURL, tlsCode, tlsConfig, logger) + case "client": + return internal.NewClient(parsedURL, logger) + case "master": + tlsCode, tlsConfig := getTLSProtocol(parsedURL, logger) + return internal.NewMaster(parsedURL, tlsCode, tlsConfig, logger, version) + default: + return nil, fmt.Errorf("unknown core: %v", parsedURL) + } } // getTLSProtocol 获取TLS配置 -func getTLSProtocol(parsedURL *url.URL) (string, *tls.Config) { +func getTLSProtocol(parsedURL *url.URL, logger *logs.Logger) (string, *tls.Config) { // 生成基本TLS配置 tlsConfig, err := cert.NewTLSConfig(version) if err != nil { @@ -99,3 +142,40 @@ func getTLSProtocol(parsedURL *url.URL) (string, *tls.Config) { return "0", nil } } + +// exit 退出程序并显示帮助信息 +func exit(err error) { + errMsg1, errMsg2 := "", "" + if err != nil { + errStr := "FAILED: " + err.Error() + if len(errStr) > 35 { + errMsg1 = errStr[:35] + if len(errStr) > 70 { + errMsg2 = errStr[35:67] + "..." + } else { + errMsg2 = errStr[35:] + } + } else { + errMsg1 = errStr + } + } + fmt.Printf(` +╭─────────────────────────────────────╮ +│ ░░█▀█░█▀█░░▀█░█▀▀░█▀█░█▀█░█▀▀░█▀▀░░ │ +│ ░░█░█░█░█░█▀█░█▀▀░█▀▀░█▀█░▀▀█░▀▀█░░ │ +│ ░░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀░░░▀░▀░▀▀▀░▀▀▀░░ │ +├─────────────────────────────────────┤ +│%*s │ +│%*s │ +├─────────────────────────────────────┤ +│ server://password@host/host? │ +│ client://password@host/host? │ +│ master://hostname:port/path? │ +├─────────────────────────────────────┤ +│ %-35s │ +│ %-35s │ +╰─────────────────────────────────────╯ + +`, 36, version, 36, fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), errMsg1, errMsg2) + os.Exit(1) +} diff --git a/nodepass/cmd/nodepass/main.go b/nodepass/cmd/nodepass/main.go index d677e1dbcb..231f525370 100644 --- a/nodepass/cmd/nodepass/main.go +++ b/nodepass/cmd/nodepass/main.go @@ -1,84 +1,11 @@ package main -import ( - "net/url" - "os" - "runtime" +import "os" - "github.com/NodePassProject/logs" -) +var version = "dev" -var ( - // 全局日志记录器 - logger = logs.NewLogger(logs.Info, true) - // 程序版本 - version = "dev" -) - -// main 程序入口 func main() { - parsedURL := getParsedURL(os.Args) - initLogLevel(parsedURL.Query().Get("log")) - coreDispatch(parsedURL) -} - -// getParsedURL 解析URL参数 -func getParsedURL(args []string) *url.URL { - if len(args) < 2 { - getExitInfo() - } - - parsedURL, err := url.Parse(args[1]) - if err != nil { - logger.Error("URL parse: %v", err) - getExitInfo() - } - - return parsedURL -} - -// initLogLevel 初始化日志级别 -func initLogLevel(level string) { - switch level { - case "none": - logger.SetLogLevel(logs.None) - case "debug": - logger.SetLogLevel(logs.Debug) - logger.Debug("Init log level: DEBUG") - case "warn": - logger.SetLogLevel(logs.Warn) - logger.Warn("Init log level: WARN") - case "error": - logger.SetLogLevel(logs.Error) - logger.Error("Init log level: ERROR") - case "event": - logger.SetLogLevel(logs.Event) - logger.Event("Init log level: EVENT") - default: - logger.SetLogLevel(logs.Info) - logger.Info("Init log level: INFO") + if err := start(os.Args); err != nil { + exit(err) } } - -// getExitInfo 输出帮助信息并退出程序 -func getExitInfo() { - logger.SetLogLevel(logs.Info) - logger.Info(`Version: %v %v/%v - -╭─────────────────────────────────────────────╮ -│ ░░█▀█░█▀█░░▀█░█▀▀░█▀█░█▀█░█▀▀░█▀▀░░ │ -│ ░░█░█░█░█░█▀█░█▀▀░█▀▀░█▀█░▀▀█░▀▀█░░ │ -│ ░░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀░░░▀░▀░▀▀▀░▀▀▀░░ │ -├─────────────────────────────────────────────┤ -│ >Universal TCP/UDP Tunneling Solution │ -│ >https://github.com/yosebyte/nodepass │ -├─────────────────────────────────────────────┤ -│ Usage: nodepass "" │ -├─────────────────────────────────────────────┤ -│ server://password@host/host?& │ -│ client://password@host/host?& │ -│ master://hostname:port/path?& │ -╰─────────────────────────────────────────────╯ -`, version, runtime.GOOS, runtime.GOARCH) - os.Exit(1) -} diff --git a/nodepass/docs/en/api.md b/nodepass/docs/en/api.md index 6fcdeb4edd..37c4ca04cf 100644 --- a/nodepass/docs/en/api.md +++ b/nodepass/docs/en/api.md @@ -42,6 +42,7 @@ nodepass "master://0.0.0.0:9090/admin?log=info&tls=1" | `/instances/{id}` | DELETE | Delete instance | | `/events` | GET | SSE real-time event stream | | `/info` | GET | Get master service info | +| `/info` | POST | Update master alias | | `/tcping` | GET | TCP connection test | | `/openapi.json` | GET | OpenAPI specification | | `/docs` | GET | Swagger UI documentation | @@ -64,7 +65,13 @@ API Key authentication is enabled by default, automatically generated and saved "type": "client|server", "status": "running|stopped|error", "url": "...", + "config": "server://0.0.0.0:8080/localhost:3000?log=info&tls=1&max=1024&mode=0&read=1h&rate=0&slot=65536&proxy=0", "restart": true, + "tags": [ + {"key": "environment", "value": "production"}, + {"key": "region", "value": "us-west-2"}, + {"key": "project", "value": "web-service"} + ], "mode": 0, "ping": 0, "pool": 0, @@ -81,7 +88,9 @@ API Key authentication is enabled by default, automatically generated and saved - `ping`/`pool`: Health check data - `tcps`/`udps`: Current active connection count statistics - `tcprx`/`tcptx`/`udprx`/`udptx`: Cumulative traffic statistics +- `config`: Instance configuration URL with complete startup configuration - `restart`: Auto-restart policy +- `tags`: Optional key-value pairs for labeling and organizing instances ### Instance URL Format @@ -514,6 +523,31 @@ To properly manage lifecycles: return data.success; } + // Update instance tags + async function updateInstanceTags(instanceId, tags) { + const response = await fetch(`${API_URL}/instances/${instanceId}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': apiKey // If API Key is enabled + }, + body: JSON.stringify({ tags }) + }); + + const data = await response.json(); + return data.success; + } + + // Delete specific tags by setting their values to empty string + async function deleteInstanceTags(instanceId, tagKeys) { + const tagsToDelete = {}; + tagKeys.forEach(key => { + tagsToDelete[key] = ""; // Empty string removes the tag + }); + + return await updateInstanceTags(instanceId, tagsToDelete); + } + // Update instance URL configuration async function updateInstanceURL(instanceId, newURL) { const response = await fetch(`${API_URL}/instances/${instanceId}`, { @@ -530,7 +564,44 @@ To properly manage lifecycles: } ``` -5. **Auto-restart Policy Management**: Configure automatic startup behavior +5. **Tag Management**: Label and organize instances using key-value pairs + ```javascript + // Update instance tags via PATCH method + async function updateInstanceTags(instanceId, tags) { + const response = await fetch(`${API_URL}/instances/${instanceId}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': apiKey + }, + body: JSON.stringify({ tags }) + }); + + return response.json(); + } + + // Add or update tags - existing tags are preserved unless specified + await updateInstanceTags('abc123', [ + {"key": "environment", "value": "production"}, // Add/update + {"key": "region", "value": "us-west-2"}, // Add/update + {"key": "team", "value": "backend"} // Add/update + ]); + + // Delete specific tags by setting empty values + await updateInstanceTags('abc123', [ + {"key": "old-tag", "value": ""}, // Delete this tag + {"key": "temp-env", "value": ""} // Delete this tag + ]); + + // Mix operations: add/update some tags, delete others + await updateInstanceTags('abc123', [ + {"key": "environment", "value": "staging"}, // Update existing + {"key": "version", "value": "2.0"}, // Add new + {"key": "deprecated", "value": ""} // Delete existing + ]); + ``` + +6. **Auto-restart Policy Management**: Configure automatic startup behavior ```javascript async function setAutoStartPolicy(instanceId, enableAutoStart) { const response = await fetch(`${API_URL}/instances/${instanceId}`, { @@ -782,6 +853,17 @@ async function configureAutoStartPolicies(instances) { } ``` +### Tag Management Rules + +1. **Merge-based Updates**: Tags are processed with merge logic - existing tags are preserved unless explicitly updated or deleted +2. **Array Format**: Tags use array format `[{"key": "key1", "value": "value1"}]` +3. **Key Filtering**: Empty keys are automatically filtered out and rejected by validation +4. **Delete by Empty Value**: Set `value: ""` (empty string) to delete an existing tag key +5. **Add/Update Logic**: Non-empty values will add new tags or update existing ones +6. **Uniqueness Check**: Duplicate keys are not allowed within the same tag operation +7. **Limits**: Maximum 50 tags, key names ≤100 characters, values ≤500 characters +8. **Persistence**: All tag operations are automatically saved to disk and restored after restart + ## Instance Data Structure The instance object in API responses contains the following fields: @@ -793,7 +875,13 @@ The instance object in API responses contains the following fields: "type": "server", // Instance type: server or client "status": "running", // Instance status: running, stopped, or error "url": "server://...", // Instance configuration URL + "config": "server://0.0.0.0:8080/localhost:3000?log=info&tls=1&max=1024&mode=0&read=1h&rate=0&slot=65536&proxy=0", // Complete configuration URL "restart": true, // Auto-restart policy + "tags": [ // Tag array + {"key": "environment", "value": "production"}, + {"key": "project", "value": "web-service"}, + {"key": "region", "value": "us-west-2"} + ], "mode": 0, // Instance mode "tcprx": 1024, // TCP received bytes "tcptx": 2048, // TCP transmitted bytes @@ -804,8 +892,31 @@ The instance object in API responses contains the following fields: **Note:** - `alias` field is optional, empty string if not set +- `config` field contains the instance's complete configuration URL, auto-generated by the system - `mode` field indicates the current runtime mode of the instance - `restart` field controls the auto-restart behavior of the instance +- `tags` field is optional, only included when tags are present + +### Instance Configuration Field + +NodePass Master automatically maintains the `config` field for each instance: + +- **Auto Generation**: Automatically generated when instances are created and updated, no manual maintenance required +- **Complete Configuration**: Contains the instance's complete URL with all default parameters +- **Configuration Inheritance**: log and tls configurations are inherited from master settings +- **Default Parameters**: Other parameters use system defaults +- **Read-Only Nature**: Auto-generated field that cannot be directly modified through the API + +**Example config field value:** +``` +server://0.0.0.0:8080/localhost:3000?log=info&tls=1&max=1024&mode=0&read=1h&rate=0&slot=65536&proxy=0 +``` + +This feature is particularly useful for: +- Configuration backup and export +- Instance configuration integrity checks +- Automated deployment scripts +- Configuration documentation generation ## System Information Endpoint @@ -825,13 +936,14 @@ The response contains the following system information fields: ```json { + "alias": "dev", // Master alias "os": "linux", // Operating system type "arch": "amd64", // System architecture "cpu": 45, // CPU usage percentage (Linux only) "mem_total": 8589934592, // Total memory in bytes (Linux only) - "mem_free": 2684354560, // Free memory in bytes (Linux only) + "mem_used": 2684354560, // Used memory in bytes (Linux only) "swap_total": 3555328000, // Total swap space in bytes (Linux only) - "swap_free": 3555328000, // Free swap space in bytes (Linux only) + "swap_used": 3555328000, // Used swap space in bytes (Linux only) "netrx": 1048576000, // Network received bytes (cumulative, Linux only) "nettx": 2097152000, // Network transmitted bytes (cumulative, Linux only) "diskr": 4194304000, // Disk read bytes (cumulative, Linux only) @@ -879,12 +991,16 @@ function displaySystemStatus() { console.log(`CPU usage: ${info.cpu}%`); } if (info.mem_total > 0) { - const memUsagePercent = ((info.mem_total - info.mem_free) / info.mem_total * 100).toFixed(1); - console.log(`Memory usage: ${memUsagePercent}% (${(info.mem_free / 1024 / 1024 / 1024).toFixed(1)}GB free of ${(info.mem_total / 1024 / 1024 / 1024).toFixed(1)}GB total)`); + const memUsagePercent = (info.mem_used / info.mem_total * 100).toFixed(1); + const memFreeGB = ((info.mem_total - info.mem_used) / 1024 / 1024 / 1024).toFixed(1); + const memTotalGB = (info.mem_total / 1024 / 1024 / 1024).toFixed(1); + console.log(`Memory usage: ${memUsagePercent}% (${memFreeGB}GB free of ${memTotalGB}GB total)`); } if (info.swap_total > 0) { - const swapUsagePercent = ((info.swap_total - info.swap_free) / info.swap_total * 100).toFixed(1); - console.log(`Swap usage: ${swapUsagePercent}% (${(info.swap_free / 1024 / 1024 / 1024).toFixed(1)}GB free of ${(info.swap_total / 1024 / 1024 / 1024).toFixed(1)}GB total)`); + const swapUsagePercent = (info.swap_used / info.swap_total * 100).toFixed(1); + const swapFreeGB = ((info.swap_total - info.swap_used) / 1024 / 1024 / 1024).toFixed(1); + const swapTotalGB = (info.swap_total / 1024 / 1024 / 1024).toFixed(1); + console.log(`Swap usage: ${swapUsagePercent}% (${swapFreeGB}GB free of ${swapTotalGB}GB total)`); } } else { console.log('CPU, memory, swap space, network I/O, disk I/O, and system uptime monitoring is only available on Linux systems'); @@ -910,8 +1026,8 @@ function displaySystemStatus() { - **Log level verification**: Ensure current log level meets expectations - **Resource monitoring**: On Linux systems, monitor CPU, memory, swap space, network I/O, disk I/O usage to ensure optimal performance - CPU usage is calculated by parsing `/proc/stat` (percentage of non-idle time) - - Memory information is obtained by parsing `/proc/meminfo` (total and free memory in bytes) - - Swap space information is obtained by parsing `/proc/meminfo` (total and free swap space in bytes) + - Memory information is obtained by parsing `/proc/meminfo` (total and used memory in bytes, calculated as total minus available) + - Swap space information is obtained by parsing `/proc/meminfo` (total and used swap space in bytes, calculated as total minus free) - Network I/O is calculated by parsing `/proc/net/dev` (cumulative bytes, excluding virtual interfaces) - Disk I/O is calculated by parsing `/proc/diskstats` (cumulative bytes, major devices only) - System uptime is obtained by parsing `/proc/uptime` @@ -987,13 +1103,12 @@ const instance = await fetch(`${API_URL}/instances/abc123`, { ``` #### PATCH /instances/{id} -- **Description**: Update instance state, alias, or perform control operations +- **Description**: Update instance state, alias, tags, or perform control operations - **Authentication**: Requires API Key -- **Request body**: `{ "alias": "new alias", "action": "start|stop|restart|reset", "restart": true|false }` -- **Features**: Does not interrupt running instances, only updates specified fields. `action: "reset"` can reset traffic statistics (tcprx, tcptx, udprx, udptx) for the instance to zero. +- **Request body**: `{ "alias": "new alias", "action": "start|stop|restart|reset", "restart": true|false, "tags": [{"key": "key1", "value": "value1"}] }` - **Example**: ```javascript -// Update alias and auto-restart policy +// Update tags await fetch(`${API_URL}/instances/abc123`, { method: 'PATCH', headers: { @@ -1001,29 +1116,27 @@ await fetch(`${API_URL}/instances/abc123`, { 'X-API-Key': apiKey }, body: JSON.stringify({ - alias: 'Web Server', - restart: true + tags: [ + {"key": "environment", "value": "production"}, + {"key": "region", "value": "us-west-2"} + ] }) }); -// Control instance operations +// Update and delete tags in one operation await fetch(`${API_URL}/instances/abc123`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey }, - body: JSON.stringify({ action: 'restart' }) -}); - -// Reset traffic statistics -await fetch(`${API_URL}/instances/abc123`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - 'X-API-Key': apiKey - }, - body: JSON.stringify({ action: 'reset' }) + body: JSON.stringify({ + tags: [ + {"key": "environment", "value": "staging"}, // Update existing + {"key": "version", "value": "2.0"}, // Add new + {"key": "old-tag", "value": ""} // Delete existing + ] + }) }); ``` @@ -1074,6 +1187,28 @@ await fetch(`${API_URL}/instances/abc123`, { - **Authentication**: Requires API Key - **Response**: Contains system information, version, uptime, CPU and RAM usage, etc. +#### POST /info +- **Description**: Update master alias +- **Authentication**: Requires API Key +- **Request body**: `{ "alias": "new alias" }` +- **Response**: Complete master information (same as GET /info) +- **Example**: +```javascript +// Update master alias +const response = await fetch(`${API_URL}/info`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': apiKey + }, + body: JSON.stringify({ alias: 'My NodePass Server' }) +}); + +const data = await response.json(); +console.log('Updated alias:', data.alias); +// Response contains full system info with updated alias +``` + #### GET /tcping - **Description**: TCP connection test, checks connectivity and latency to target address - **Authentication**: Requires API Key @@ -1130,9 +1265,10 @@ Examples: | `tls` | TLS encryption level | `0`(none), `1`(self-signed), `2`(certificate) | `0` | Server only | | `crt` | Certificate path | File path | None | Server only | | `key` | Private key path | File path | None | Server only | -| `mode` | Runtime mode control | `0`(auto), `1`(force mode 1), `2`(force mode 2) | `0` | Both | | `min` | Minimum pool capacity | Integer > 0 | `64` | Client dual-end handshake mode only | | `max` | Maximum pool capacity | Integer > 0 | `1024` | Dual-end handshake mode | +| `mode` | Runtime mode control | `0`(auto), `1`(force mode 1), `2`(force mode 2) | `0` | Both | | `read` | Read timeout duration | Time duration (e.g., `10m`, `30s`, `1h`) | `10m` | Both | | `rate` | Bandwidth rate limit | Integer (Mbps), 0=unlimited | `0` | Both | +| `slot` | Connection slot count | Integer (1-65536) | `65536` | Both | | `proxy` | PROXY protocol support | `0`(disabled), `1`(enabled) | `0` | Both | \ No newline at end of file diff --git a/nodepass/docs/en/configuration.md b/nodepass/docs/en/configuration.md index 8f19b23c13..d8a779ba64 100644 --- a/nodepass/docs/en/configuration.md +++ b/nodepass/docs/en/configuration.md @@ -254,19 +254,19 @@ nodepass "server://0.0.0.0:10101/0.0.0.0:8080?log=info&tls=1&proxy=1&rate=100" NodePass allows flexible configuration via URL query parameters. The following table shows which parameters are applicable in server, client, and master modes: -| Parameter | Description | server | client | master | -|-----------|----------------------|:------:|:------:|:------:| -| `log` | Log level | O | O | O | -| `tls` | TLS encryption mode | O | X | O | -| `crt` | Custom certificate path| O | X | O | -| `key` | Custom key path | O | X | O | -| `min` | Minimum pool capacity | X | O | X | -| `max` | Maximum pool capacity | O | X | X | -| `mode` | Run mode control | O | O | X | -| `read` | Data read timeout | O | O | X | -| `rate` | Bandwidth rate limit | O | O | X | -| `slot` | Maximum connection limit | O | O | X | -| `proxy` | PROXY protocol support| O | O | X | +| Parameter | Description | Default | server | client | master | +|-----------|----------------------|---------|:------:|:------:|:------:| +| `log` | Log level | `info` | O | O | O | +| `tls` | TLS encryption mode | `0` | O | X | O | +| `crt` | Custom certificate path| N/A | O | X | O | +| `key` | Custom key path | N/A | O | X | O | +| `min` | Minimum pool capacity | `64` | X | O | X | +| `max` | Maximum pool capacity | `1024` | O | X | X | +| `mode` | Run mode control | `0` | O | O | X | +| `read` | Data read timeout | `1h` | O | O | X | +| `rate` | Bandwidth rate limit | `0` | O | O | X | +| `slot` | Maximum connection limit | `65536` | O | O | X | +| `proxy` | PROXY protocol support| `0` | O | O | X | - O: Parameter is valid and recommended for configuration - X: Parameter is not applicable and should be ignored @@ -285,6 +285,7 @@ NodePass behavior can be fine-tuned using environment variables. Below is the co | Variable | Description | Default | Example | |----------|-------------|---------|---------| | `NP_SEMAPHORE_LIMIT` | Signal channel buffer size | 65536 | `export NP_SEMAPHORE_LIMIT=2048` | +| `NP_TCP_DATA_BUF_SIZE` | Buffer size for TCP data transfer | 32768 | `export NP_TCP_DATA_BUF_SIZE=65536` | | `NP_UDP_DATA_BUF_SIZE` | Buffer size for UDP packets | 2048 | `export NP_UDP_DATA_BUF_SIZE=16384` | | `NP_HANDSHAKE_TIMEOUT` | Timeout for handshake operations | 10s | `export NP_HANDSHAKE_TIMEOUT=30s` | | `NP_TCP_DIAL_TIMEOUT` | Timeout for establishing TCP connections | 30s | `export NP_TCP_DIAL_TIMEOUT=60s` | @@ -348,6 +349,11 @@ For applications relying heavily on UDP traffic: For optimizing TCP connections: +- `NP_TCP_DATA_BUF_SIZE`: Buffer size for TCP data transfer + - Default (32768) provides good balance for most applications + - Increase for high-throughput applications requiring larger buffers + - Consider increasing to 65536 or higher for bulk data transfers and streaming + - `NP_TCP_DIAL_TIMEOUT`: Timeout for establishing TCP connections - Default (30s) is suitable for most network conditions - Increase for unstable network conditions @@ -401,6 +407,7 @@ Environment variables: export NP_MIN_POOL_INTERVAL=50ms export NP_MAX_POOL_INTERVAL=500ms export NP_SEMAPHORE_LIMIT=8192 +export NP_TCP_DATA_BUF_SIZE=65536 export NP_UDP_DATA_BUF_SIZE=32768 export NP_POOL_GET_TIMEOUT=60s export NP_REPORT_INTERVAL=10s diff --git a/nodepass/docs/en/usage.md b/nodepass/docs/en/usage.md index 94015b9bf7..c66662cbae 100644 --- a/nodepass/docs/en/usage.md +++ b/nodepass/docs/en/usage.md @@ -327,7 +327,7 @@ NodePass uses tunnel keys to authenticate connections between clients and server The handshake process between client and server is as follows: 1. **Client Connection**: Client connects to the server's tunnel address -2. **Key Authentication**: Client sends XOR-encrypted tunnel key +2. **Key Authentication**: Client sends encrypted tunnel key 3. **Server Verification**: Server decrypts and verifies if the key matches 4. **Configuration Sync**: Upon successful verification, server sends tunnel configuration including: - Data flow direction diff --git a/nodepass/docs/zh/api.md b/nodepass/docs/zh/api.md index fb7c7528ef..00723d4df9 100644 --- a/nodepass/docs/zh/api.md +++ b/nodepass/docs/zh/api.md @@ -42,6 +42,7 @@ nodepass "master://0.0.0.0:9090/admin?log=info&tls=1" | `/instances/{id}` | DELETE | 删除实例 | | `/events` | GET | SSE 实时事件流 | | `/info` | GET | 获取主控服务信息 | +| `/info` | POST | 更新主控别名 | | `/tcping` | GET | TCP连接测试 | | `/openapi.json` | GET | OpenAPI 规范 | | `/docs` | GET | Swagger UI 文档 | @@ -64,7 +65,13 @@ API Key 认证默认启用,首次启动自动生成并保存在 `nodepass.gob` "type": "client|server", "status": "running|stopped|error", "url": "...", + "config": "server://0.0.0.0:8080/localhost:3000?log=info&tls=1&max=1024&mode=0&read=1h&rate=0&slot=65536&proxy=0", "restart": true, + "tags": [ + {"key": "environment", "value": "production"}, + {"key": "region", "value": "us-west-2"}, + {"key": "project", "value": "web-service"} + ], "mode": 0, "ping": 0, "pool": 0, @@ -81,7 +88,9 @@ API Key 认证默认启用,首次启动自动生成并保存在 `nodepass.gob` - `ping`/`pool`:健康检查数据 - `tcps`/`udps`:当前活动连接数统计 - `tcprx`/`tcptx`/`udprx`/`udptx`:累计流量统计 +- `config`:实例配置URL,包含完整的启动配置 - `restart`:自启动策略 +- `tags`:可选的键值对,用于标记和组织实例 ### 实例 URL 格式 @@ -112,6 +121,7 @@ API Key 认证默认启用,首次启动自动生成并保存在 `nodepass.gob` - 所有实例、流量、健康检查、别名、自启动策略均持久化存储,重启后自动恢复 - API 详细规范见 `/openapi.json`,Swagger UI 见 `/docs` + ```javascript // 重新生成API Key(需要知道当前的API Key) async function regenerateApiKey() { @@ -513,6 +523,31 @@ NodePass主控模式提供自动备份功能,定期备份状态文件以防止 return data.success; } + // 更新实例标签 + async function updateInstanceTags(instanceId, tags) { + const response = await fetch(`${API_URL}/instances/${instanceId}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': apiKey // 如果启用了API Key + }, + body: JSON.stringify({ tags }) + }); + + const data = await response.json(); + return data.success; + } + + // 通过设置空字符串值删除特定标签 + async function deleteInstanceTags(instanceId, tagKeys) { + const tagsToDelete = {}; + tagKeys.forEach(key => { + tagsToDelete[key] = ""; // 空字符串会删除标签 + }); + + return await updateInstanceTags(instanceId, tagsToDelete); + } + // 更新实例URL配置 async function updateInstanceURL(instanceId, newURL) { const response = await fetch(`${API_URL}/instances/${instanceId}`, { @@ -529,7 +564,44 @@ NodePass主控模式提供自动备份功能,定期备份状态文件以防止 } ``` -5. **自启动策略管理**:配置自动启动行为 +5. **标签管理**:使用键值对标记和组织实例 + ```javascript + // 通过PATCH方法更新实例标签 + async function updateInstanceTags(instanceId, tags) { + const response = await fetch(`${API_URL}/instances/${instanceId}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': apiKey + }, + body: JSON.stringify({ tags }) + }); + + return response.json(); + } + + // 添加或更新标签 - 现有标签会保留,除非被显式指定 + await updateInstanceTags('abc123', [ + {"key": "environment", "value": "production"}, // 添加/更新 + {"key": "region", "value": "us-west-2"}, // 添加/更新 + {"key": "team", "value": "backend"} // 添加/更新 + ]); + + // 通过设置空值删除特定标签 + await updateInstanceTags('abc123', [ + {"key": "old-tag", "value": ""}, // 删除此标签 + {"key": "temp-env", "value": ""} // 删除此标签 + ]); + + // 混合操作:添加/更新某些标签,删除其他标签 + await updateInstanceTags('abc123', [ + {"key": "environment", "value": "staging"}, // 更新现有 + {"key": "version", "value": "2.0"}, // 添加新的 + {"key": "deprecated", "value": ""} // 删除现有 + ]); + ``` + +6. **自启动策略管理**:配置自动启动行为 ```javascript async function setAutoStartPolicy(instanceId, enableAutoStart) { const response = await fetch(`${API_URL}/instances/${instanceId}`, { @@ -781,6 +853,17 @@ async function configureAutoStartPolicies(instances) { } ``` +### 标签管理规则 + +1. **合并式更新**:标签采用合并逻辑处理 - 现有标签会保留,除非被显式更新或删除 +2. **数组格式**:标签使用数组格式 `[{"key": "key1", "value": "value1"}]` +3. **键过滤**:空键会被自动过滤并被验证拒绝 +4. **空值删除**:设置 `value: ""` (空字符串)可删除现有标签键 +5. **添加/更新逻辑**:非空值会添加新标签或更新现有标签 +6. **唯一性检查**:同一标签操作中不允许重复的键名 +7. **限制**:最多50个标签,键名长度≤100字符,值长度≤500字符 +8. **持久化**:所有标签操作自动保存到磁盘,重启后恢复 + ## 实例数据结构 API响应中的实例对象包含以下字段: @@ -792,7 +875,13 @@ API响应中的实例对象包含以下字段: "type": "server", // 实例类型:server 或 client "status": "running", // 实例状态:running、stopped 或 error "url": "server://...", // 实例配置URL + "config": "server://0.0.0.0:8080/localhost:3000?log=info&tls=1&max=1024&mode=0&read=1h&rate=0&slot=65536&proxy=0", // 完整配置URL "restart": true, // 自启动策略 + "tags": [ // 标签数组 + {"key": "environment", "value": "production"}, + {"key": "project", "value": "web-service"}, + {"key": "region", "value": "us-west-2"} + ], "mode": 0, // 运行模式 "tcprx": 1024, // TCP接收字节数 "tcptx": 2048, // TCP发送字节数 @@ -803,8 +892,31 @@ API响应中的实例对象包含以下字段: **注意:** - `alias` 字段为可选,如果未设置则为空字符串 +- `config` 字段包含实例的完整配置URL,由系统自动生成 - `mode` 字段表示实例当前的运行模式 - `restart` 字段控制实例的自启动行为 +- `tags` 字段为可选,仅在设置标签时存在 + +### 实例配置字段 + +NodePass主控会自动为每个实例维护 `config` 字段: + +- **自动生成**:在实例创建和更新时自动生成,无需手动维护 +- **完整配置**:包含实例的完整URL,带有所有默认参数 +- **配置继承**:log和tls配置继承自主控设置 +- **默认参数**:其他参数使用系统默认值 +- **只读性质**:自动生成的字段,通过API无法直接修改 + +**示例 config 字段值:** +``` +server://0.0.0.0:8080/localhost:3000?log=info&tls=1&max=1024&mode=0&read=1h&rate=0&slot=65536&proxy=0 +``` + +此功能特别适用于: +- 配置备份和导出 +- 实例配置的完整性检查 +- 自动化部署脚本 +- 配置文档生成 ## 系统信息端点 @@ -824,13 +936,14 @@ GET /info ```json { + "alias": "dev", // 主控别名 "os": "linux", // 操作系统类型 "arch": "amd64", // 系统架构 "cpu": 45, // CPU使用率百分比(仅Linux系统) "mem_total": 8589934592, // 内存容量(字节,仅Linux系统) - "mem_free": 2684354560, // 内存可用(字节,仅Linux系统) + "mem_used": 2684354560, // 内存已用(字节,仅Linux系统) "swap_total": 3555328000, // 交换区总量(字节,仅Linux系统) - "swap_free": 3555328000, // 交换区可用(字节,仅Linux系统) + "swap_used": 3555328000, // 交换区已用(字节,仅Linux系统) "netrx": 1048576000, // 网络接收字节数(累计值,仅Linux) "nettx": 2097152000, // 网络发送字节数(累计值,仅Linux) "diskr": 4194304000, // 磁盘读取字节数(累计值,仅Linux) @@ -878,12 +991,16 @@ function displaySystemStatus() { console.log(`CPU使用率: ${info.cpu}%`); } if (info.mem_total > 0) { - const memUsagePercent = ((info.mem_total - info.mem_free) / info.mem_total * 100).toFixed(1); - console.log(`内存使用率: ${memUsagePercent}% (${(info.mem_free / 1024 / 1024 / 1024).toFixed(1)}GB 可用,共 ${(info.mem_total / 1024 / 1024 / 1024).toFixed(1)}GB)`); + const memUsagePercent = (info.mem_used / info.mem_total * 100).toFixed(1); + const memFreeGB = ((info.mem_total - info.mem_used) / 1024 / 1024 / 1024).toFixed(1); + const memTotalGB = (info.mem_total / 1024 / 1024 / 1024).toFixed(1); + console.log(`内存使用率: ${memUsagePercent}% (${memFreeGB}GB 可用,共 ${memTotalGB}GB)`); } if (info.swap_total > 0) { - const swapUsagePercent = ((info.swap_total - info.swap_free) / info.swap_total * 100).toFixed(1); - console.log(`交换区使用率: ${swapUsagePercent}% (${(info.swap_free / 1024 / 1024 / 1024).toFixed(1)}GB 可用,共 ${(info.swap_total / 1024 / 1024 / 1024).toFixed(1)}GB)`); + const swapUsagePercent = (info.swap_used / info.swap_total * 100).toFixed(1); + const swapFreeGB = ((info.swap_total - info.swap_used) / 1024 / 1024 / 1024).toFixed(1); + const swapTotalGB = (info.swap_total / 1024 / 1024 / 1024).toFixed(1); + console.log(`交换区使用率: ${swapUsagePercent}% (${swapFreeGB}GB 可用,共 ${swapTotalGB}GB)`); } } else { console.log('CPU、内存、交换区、网络I/O、磁盘I/O和系统运行时间监控功能仅在Linux系统上可用'); @@ -909,8 +1026,8 @@ function displaySystemStatus() { - **日志级别验证**:确认当前日志级别符合预期 - **资源监控**:在Linux系统上,监控CPU、内存、交换区、网络I/O、磁盘I/O使用情况以确保最佳性能 - CPU使用率通过解析`/proc/stat`计算(非空闲时间百分比) - - 内存信息通过解析`/proc/meminfo`获取(总量和可用量,单位为字节) - - 交换区信息通过解析`/proc/meminfo`获取(总量和可用量,单位为字节) + - 内存信息通过解析`/proc/meminfo`获取(总量和已用量,单位为字节,已用量计算为总量减去可用量) + - 交换区信息通过解析`/proc/meminfo`获取(总量和已用量,单位为字节,已用量计算为总量减去空闲量) - 网络I/O通过解析`/proc/net/dev`计算(累计字节数,排除虚拟接口) - 磁盘I/O通过解析`/proc/diskstats`计算(累计字节数,仅统计主设备) - 系统运行时间通过解析`/proc/uptime`获取 @@ -986,13 +1103,12 @@ const instance = await fetch(`${API_URL}/instances/abc123`, { ``` #### PATCH /instances/{id} -- **描述**:更新实例状态、别名或执行控制操作 +- **描述**:更新实例状态、别名、标签或执行控制操作 - **认证**:需要API Key -- **请求体**:`{ "alias": "新别名", "action": "start|stop|restart|reset", "restart": true|false }` -- **特点**:不中断正在运行的实例,仅更新指定字段。`action: "reset"` 可将该实例的流量统计(tcprx、tcptx、udprx、udptx)清零。 +- **请求体**:`{ "alias": "新别名", "action": "start|stop|restart|reset", "restart": true|false, "tags": [{"key": "键", "value": "值"}] }` - **示例**: ```javascript -// 更新别名和自启动策略 +// 更新标签 await fetch(`${API_URL}/instances/abc123`, { method: 'PATCH', headers: { @@ -1000,29 +1116,27 @@ await fetch(`${API_URL}/instances/abc123`, { 'X-API-Key': apiKey }, body: JSON.stringify({ - alias: 'Web服务器', - restart: true + tags: [ + {"key": "environment", "value": "production"}, + {"key": "region", "value": "us-west-2"} + ] }) }); -// 控制实例操作 +// 一次操作中更新和删除标签 await fetch(`${API_URL}/instances/abc123`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey }, - body: JSON.stringify({ action: 'restart' }) -}); - -// 清零流量统计 -await fetch(`${API_URL}/instances/abc123`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - 'X-API-Key': apiKey - }, - body: JSON.stringify({ action: 'reset' }) + body: JSON.stringify({ + tags: [ + {"key": "environment", "value": "staging"}, // 更新现有 + {"key": "version", "value": "2.0"}, // 添加新的 + {"key": "old-tag", "value": ""} // 删除现有 + ] + }) }); ``` @@ -1073,6 +1187,28 @@ await fetch(`${API_URL}/instances/abc123`, { - **认证**:需要API Key - **响应**:包含系统信息、版本、运行时间、CPU和RAM使用率等 +#### POST /info +- **描述**:更新主控别名 +- **认证**:需要API Key +- **请求体**:`{ "alias": "新别名" }` +- **响应**:完整的主控信息(与GET /info相同) +- **示例**: +```javascript +// 更新主控别名 +const response = await fetch(`${API_URL}/info`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': apiKey + }, + body: JSON.stringify({ alias: '我的NodePass服务器' }) +}); + +const data = await response.json(); +console.log('更新后的别名:', data.alias); +// 响应包含完整的系统信息,包括更新后的别名 +``` + #### GET /tcping - **描述**:TCP连接测试,检测目标地址的连通性和延迟 - **认证**:需要API Key @@ -1129,9 +1265,10 @@ client://:/:? | `tls` | TLS加密级别 | `0`(无), `1`(自签名), `2`(证书) | `0` | 仅服务器 | | `crt` | 证书路径 | 文件路径 | 无 | 仅服务器 | | `key` | 私钥路径 | 文件路径 | 无 | 仅服务器 | -| `mode` | 运行模式控制 | `0`(自动), `1`(强制模式1), `2`(强制模式2) | `0` | 两者 | | `min` | 最小连接池容量 | 整数 > 0 | `64` | 仅客户端双端握手模式 | | `max` | 最大连接池容量 | 整数 > 0 | `1024` | 双端握手模式 | +| `mode` | 运行模式控制 | `0`(自动), `1`(强制模式1), `2`(强制模式2) | `0` | 两者 | | `read` | 读取超时时间 | 时间长度 (如 `10m`, `30s`, `1h`) | `10m` | 两者 | | `rate` | 带宽速率限制 | 整数 (Mbps), 0=无限制 | `0` | 两者 | -| `proxy` | PROXY协议支持 | `0`(禁用), `1`(启用) | `0` | 两者 | +| `slot` | 连接槽位数 | 整数 (1-65536) | `65536` | 两者 | +| `proxy` | PROXY协议支持 | `0`(禁用), `1`(启用) | `0` | 两者 | \ No newline at end of file diff --git a/nodepass/docs/zh/configuration.md b/nodepass/docs/zh/configuration.md index 3117aaa22d..40c34f3fa5 100644 --- a/nodepass/docs/zh/configuration.md +++ b/nodepass/docs/zh/configuration.md @@ -254,19 +254,19 @@ nodepass "server://0.0.0.0:10101/0.0.0.0:8080?log=info&tls=1&proxy=1&rate=100" NodePass支持通过URL查询参数进行灵活配置,不同参数在 server、client、master 模式下的适用性如下表: -| 参数 | 说明 | server | client | master | -|-----------|----------------------|:------:|:------:|:------:| -| `log` | 日志级别 | O | O | O | -| `tls` | TLS加密模式 | O | X | O | -| `crt` | 自定义证书路径 | O | X | O | -| `key` | 自定义密钥路径 | O | X | O | -| `min` | 最小连接池容量 | X | O | X | -| `max` | 最大连接池容量 | O | X | X | -| `mode` | 运行模式控制 | O | O | X | -| `read` | 读取超时时间 | O | O | X | -| `rate` | 带宽速率限制 | O | O | X | -| `slot` | 最大连接数限制 | O | O | X | -| `proxy` | PROXY协议支持 | O | O | X | +| 参数 | 说明 | 默认值 | server | client | master | +|-----------|----------------------|-----------|:------:|:------:|:------:| +| `log` | 日志级别 | `info` | O | O | O | +| `tls` | TLS加密模式 | `0` | O | X | O | +| `crt` | 自定义证书路径 | N/A | O | X | O | +| `key` | 自定义密钥路径 | N/A | O | X | O | +| `min` | 最小连接池容量 | `64` | X | O | X | +| `max` | 最大连接池容量 | `1024` | O | X | X | +| `mode` | 运行模式控制 | `0` | O | O | X | +| `read` | 读取超时时间 | `1h` | O | O | X | +| `rate` | 带宽速率限制 | `0` | O | O | X | +| `slot` | 最大连接数限制 | `65536` | O | O | X | +| `proxy` | PROXY协议支持 | `0` | O | O | X | - O:参数有效,推荐根据实际场景配置 @@ -286,6 +286,7 @@ NodePass支持通过URL查询参数进行灵活配置,不同参数在 server | 变量 | 描述 | 默认值 | 示例 | |----------|-------------|---------|---------| | `NP_SEMAPHORE_LIMIT` | 信号缓冲区大小 | 65536 | `export NP_SEMAPHORE_LIMIT=2048` | +| `NP_TCP_DATA_BUF_SIZE` | TCP数据传输缓冲区大小 | 32768 | `export NP_TCP_DATA_BUF_SIZE=65536` | | `NP_UDP_DATA_BUF_SIZE` | UDP数据包缓冲区大小 | 2048 | `export NP_UDP_DATA_BUF_SIZE=16384` | | `NP_HANDSHAKE_TIMEOUT` | 握手操作超时 | 10s | `export NP_HANDSHAKE_TIMEOUT=30s` | | `NP_TCP_DIAL_TIMEOUT` | TCP连接建立超时 | 30s | `export NP_TCP_DIAL_TIMEOUT=60s` | @@ -349,6 +350,11 @@ NodePass支持通过URL查询参数进行灵活配置,不同参数在 server 对于TCP连接的优化: +- `NP_TCP_DATA_BUF_SIZE`:TCP数据传输缓冲区大小 + - 默认值(32768)为大多数应用提供良好平衡 + - 对于需要大缓冲区的高吞吐量应用增加此值 + - 考虑为批量数据传输和流媒体增加到65536或更高 + - `NP_TCP_DIAL_TIMEOUT`:TCP连接建立超时 - 默认值(30s)适用于大多数网络条件 - 对于网络条件不稳定的环境增加此值 @@ -402,6 +408,7 @@ nodepass "client://server.example.com:10101/127.0.0.1:8080?min=128&rate=500&slot export NP_MIN_POOL_INTERVAL=50ms export NP_MAX_POOL_INTERVAL=500ms export NP_SEMAPHORE_LIMIT=8192 +export NP_TCP_DATA_BUF_SIZE=65536 export NP_UDP_DATA_BUF_SIZE=32768 export NP_POOL_GET_TIMEOUT=60s export NP_REPORT_INTERVAL=10s diff --git a/nodepass/docs/zh/usage.md b/nodepass/docs/zh/usage.md index a5acbd0da1..f60edeba3d 100644 --- a/nodepass/docs/zh/usage.md +++ b/nodepass/docs/zh/usage.md @@ -330,7 +330,7 @@ NodePass使用隧道密钥来验证客户端和服务端之间的连接。密钥 客户端与服务端的握手过程如下: 1. **客户端连接**:客户端连接到服务端的隧道地址 -2. **密钥验证**:客户端发送XOR加密的隧道密钥 +2. **密钥验证**:客户端发送加密的隧道密钥 3. **服务端验证**:服务端解密并验证密钥是否匹配 4. **配置同步**:验证成功后,服务端发送隧道配置信息,包括: - 数据流向模式 diff --git a/nodepass/go.mod b/nodepass/go.mod index 6eadee1c25..8b8584acfe 100644 --- a/nodepass/go.mod +++ b/nodepass/go.mod @@ -4,7 +4,7 @@ go 1.25.0 require ( github.com/NodePassProject/cert v1.0.1 - github.com/NodePassProject/conn v1.0.12 + github.com/NodePassProject/conn v1.0.15 github.com/NodePassProject/logs v1.0.3 - github.com/NodePassProject/pool v1.0.24 + github.com/NodePassProject/pool v1.0.30 ) diff --git a/nodepass/go.sum b/nodepass/go.sum index 7068e10bf9..9da59339ed 100644 --- a/nodepass/go.sum +++ b/nodepass/go.sum @@ -1,8 +1,8 @@ github.com/NodePassProject/cert v1.0.1 h1:BDy2tTOudy6yk7hvcmScAJMw4NrpCdSCsbuu7hHsIuw= github.com/NodePassProject/cert v1.0.1/go.mod h1:wP7joOJeQAIlIuOUmhHPwMExjuwGa4XApMWQYChGSrk= -github.com/NodePassProject/conn v1.0.12 h1:63Ueb//leEptKNU8MXlrEDF3exJiitZefmJAEyMLnt4= -github.com/NodePassProject/conn v1.0.12/go.mod h1:xfQ7ZLUxrtdLsljGHYYCToW+Hdg6DAbmL1Cs94n5h6E= +github.com/NodePassProject/conn v1.0.15 h1:YJaWphxGO4EZGdel/Lw4taLcb2hmHefjJipkilHn0B4= +github.com/NodePassProject/conn v1.0.15/go.mod h1:xfQ7ZLUxrtdLsljGHYYCToW+Hdg6DAbmL1Cs94n5h6E= github.com/NodePassProject/logs v1.0.3 h1:CDUZVQ477vmmFQHazrQCWM0gJPNINm0C2N3FzC4jVyw= github.com/NodePassProject/logs v1.0.3/go.mod h1:TwtPXOzLtb8iH+fdduQjEEywICXivsM39cy9AinMSks= -github.com/NodePassProject/pool v1.0.24 h1:8DSgZ2dxnzYXgplp9ZwZlpwFiKIWU4JYW1glUm+hq/4= -github.com/NodePassProject/pool v1.0.24/go.mod h1:joQFk1oocg56QpJ1QK/2g5Jv/AyqYUQgPXMG1gWe8iA= +github.com/NodePassProject/pool v1.0.30 h1:EupeGn6nTOCzybQMHWmEphPjR7CTEptgOFvkD0UMw6Q= +github.com/NodePassProject/pool v1.0.30/go.mod h1:joQFk1oocg56QpJ1QK/2g5Jv/AyqYUQgPXMG1gWe8iA= diff --git a/nodepass/internal/client.go b/nodepass/internal/client.go index e839e619de..2c37719163 100644 --- a/nodepass/internal/client.go +++ b/nodepass/internal/client.go @@ -3,7 +3,6 @@ package internal import ( "bufio" - "bytes" "context" "fmt" "io" @@ -13,6 +12,7 @@ import ( "os/signal" "strconv" "strings" + "sync" "syscall" "time" @@ -28,25 +28,39 @@ type Client struct { } // NewClient 创建新的客户端实例 -func NewClient(parsedURL *url.URL, logger *logs.Logger) *Client { +func NewClient(parsedURL *url.URL, logger *logs.Logger) (*Client, error) { client := &Client{ Common: Common{ logger: logger, signalChan: make(chan string, semaphoreLimit), + tcpBufferPool: &sync.Pool{ + New: func() any { + buf := make([]byte, tcpDataBufSize) + return &buf + }, + }, + udpBufferPool: &sync.Pool{ + New: func() any { + buf := make([]byte, udpDataBufSize) + return &buf + }, + }, }, tunnelName: parsedURL.Hostname(), } - client.initConfig(parsedURL) + if err := client.initConfig(parsedURL); err != nil { + return nil, fmt.Errorf("newClient: initConfig failed: %w", err) + } client.initRateLimiter() - return client + return client, nil } // Run 管理客户端生命周期 func (c *Client) Run() { logInfo := func(prefix string) { - c.logger.Info("%v: %v@%v/%v?min=%v&mode=%v&read=%v&rate=%v&slot=%v", + c.logger.Info("%v: client://%v@%v/%v?min=%v&mode=%v&read=%v&rate=%v&slot=%v&proxy=%v", prefix, c.tunnelKey, c.tunnelTCPAddr, c.targetTCPAddr, - c.minPoolCapacity, c.runMode, c.readTimeout, c.rateLimit/125000, c.slotLimit) + c.minPoolCapacity, c.runMode, c.readTimeout, c.rateLimit/125000, c.slotLimit, c.proxyProtocol) } logInfo("Client started") @@ -168,7 +182,7 @@ func (c *Client) tunnelHandshake() error { c.tunnelTCPConn.SetKeepAlivePeriod(reportInterval) // 发送隧道密钥 - _, err = c.tunnelTCPConn.Write(append(c.xor([]byte(c.tunnelKey)), '\n')) + _, err = c.tunnelTCPConn.Write(c.encode([]byte(c.tunnelKey))) if err != nil { return fmt.Errorf("tunnelHandshake: write tunnel key failed: %w", err) } @@ -179,8 +193,14 @@ func (c *Client) tunnelHandshake() error { return fmt.Errorf("tunnelHandshake: readBytes failed: %w", err) } + // 解码隧道URL + tunnelURLData, err := c.decode(rawTunnelURL) + if err != nil { + return fmt.Errorf("tunnelHandshake: decode tunnel URL failed: %w", err) + } + // 解析隧道URL - tunnelURL, err := url.Parse(string(c.xor(bytes.TrimSuffix(rawTunnelURL, []byte{'\n'})))) + tunnelURL, err := url.Parse(string(tunnelURLData)) if err != nil { return fmt.Errorf("tunnelHandshake: parse tunnel URL failed: %w", err) } diff --git a/nodepass/internal/common.go b/nodepass/internal/common.go index 680e049c97..ae5f45fef0 100644 --- a/nodepass/internal/common.go +++ b/nodepass/internal/common.go @@ -5,6 +5,7 @@ import ( "bufio" "bytes" "context" + "encoding/base64" "encoding/hex" "fmt" "hash/fnv" @@ -48,6 +49,8 @@ type Common struct { rateLimiter *conn.RateLimiter // 全局限速器 readTimeout time.Duration // 读取超时 bufReader *bufio.Reader // 缓冲读取器 + tcpBufferPool *sync.Pool // TCP缓冲区池 + udpBufferPool *sync.Pool // UDP缓冲区池 signalChan chan string // 信号通道 checkPoint time.Time // 检查点时间 slotLimit int32 // 槽位限制 @@ -64,6 +67,7 @@ type Common struct { // 配置变量,可通过环境变量调整 var ( semaphoreLimit = getEnvAsInt("NP_SEMAPHORE_LIMIT", 65536) // 信号量限制 + tcpDataBufSize = getEnvAsInt("NP_TCP_DATA_BUF_SIZE", 32768) // TCP缓冲区大小 udpDataBufSize = getEnvAsInt("NP_UDP_DATA_BUF_SIZE", 2048) // UDP缓冲区大小 handshakeTimeout = getEnvAsDuration("NP_HANDSHAKE_TIMEOUT", 10*time.Second) // 握手超时 tcpDialTimeout = getEnvAsDuration("NP_TCP_DIAL_TIMEOUT", 30*time.Second) // TCP拨号超时 @@ -77,28 +81,40 @@ var ( ReloadInterval = getEnvAsDuration("NP_RELOAD_INTERVAL", 1*time.Hour) // 重载间隔 ) -// UDP缓冲区池 -var udpBufferPool = sync.Pool{ - New: func() any { - b := make([]byte, udpDataBufSize) - return &b - }, +// 默认配置 +const ( + defaultMinPool = 64 // 默认最小池容量 + defaultMaxPool = 1024 // 默认最大池容量 + defaultRunMode = "0" // 默认运行模式 + defaultReadTimeout = 1 * time.Hour // 默认读取超时 + defaultRateLimit = 0 // 默认速率限制 + defaultSlotLimit = 65536 // 默认槽位限制 + defaultProxyProtocol = "0" // 默认代理协议 +) + +// getTCPBuffer 获取TCP缓冲区 +func (c *Common) getTCPBuffer() []byte { + buf := c.tcpBufferPool.Get().(*[]byte) + return (*buf)[:tcpDataBufSize] } -// getUDPBuffer 从池中获取UDP缓冲区 -func getUDPBuffer() []byte { - buf := udpBufferPool.Get().(*[]byte) - if cap(*buf) < udpDataBufSize { - b := make([]byte, udpDataBufSize) - return b +// putTCPBuffer 归还TCP缓冲区 +func (c *Common) putTCPBuffer(buf []byte) { + if buf != nil && cap(buf) >= tcpDataBufSize { + c.tcpBufferPool.Put(&buf) } +} + +// getUDPBuffer 获取UDP缓冲区 +func (c *Common) getUDPBuffer() []byte { + buf := c.udpBufferPool.Get().(*[]byte) return (*buf)[:udpDataBufSize] } -// putUDPBuffer 将UDP缓冲区归还到池中 -func putUDPBuffer(buf []byte) { - if buf != nil { - udpBufferPool.Put(&buf) +// putUDPBuffer 归还UDP缓冲区 +func (c *Common) putUDPBuffer(buf []byte) { + if buf != nil && cap(buf) >= udpDataBufSize { + c.udpBufferPool.Put(&buf) } } @@ -166,6 +182,59 @@ func (c *Common) xor(data []byte) []byte { return data } +// encode base64编码数据 +func (c *Common) encode(data []byte) []byte { + return append([]byte(base64.StdEncoding.EncodeToString(c.xor(data))), '\n') +} + +// decode base64解码数据 +func (c *Common) decode(data []byte) ([]byte, error) { + decoded, err := base64.StdEncoding.DecodeString(string(bytes.TrimSuffix(data, []byte{'\n'}))) + if err != nil { + return nil, fmt.Errorf("decode: base64 decode failed: %w", err) + } + return c.xor(decoded), nil +} + +// getAddress 解析和设置地址信息 +func (c *Common) getAddress(parsedURL *url.URL) error { + // 解析隧道地址 + tunnelAddr := parsedURL.Host + + // 解析隧道TCP地址 + if tunnelTCPAddr, err := net.ResolveTCPAddr("tcp", tunnelAddr); err == nil { + c.tunnelTCPAddr = tunnelTCPAddr + } else { + return fmt.Errorf("getAddress: resolveTCPAddr failed: %w", err) + } + + // 解析隧道UDP地址 + if tunnelUDPAddr, err := net.ResolveUDPAddr("udp", tunnelAddr); err == nil { + c.tunnelUDPAddr = tunnelUDPAddr + } else { + return fmt.Errorf("getAddress: resolveUDPAddr failed: %w", err) + } + + // 处理目标地址 + targetAddr := strings.TrimPrefix(parsedURL.Path, "/") + + // 解析目标TCP地址 + if targetTCPAddr, err := net.ResolveTCPAddr("tcp", targetAddr); err == nil { + c.targetTCPAddr = targetTCPAddr + } else { + return fmt.Errorf("getAddress: resolveTCPAddr failed: %w", err) + } + + // 解析目标UDP地址 + if targetUDPAddr, err := net.ResolveUDPAddr("udp", targetAddr); err == nil { + c.targetUDPAddr = targetUDPAddr + } else { + return fmt.Errorf("getAddress: resolveUDPAddr failed: %w", err) + } + + return nil +} + // getTunnelKey 从URL中获取隧道密钥 func (c *Common) getTunnelKey(parsedURL *url.URL) { if key := parsedURL.User.Username(); key != "" { @@ -177,43 +246,6 @@ func (c *Common) getTunnelKey(parsedURL *url.URL) { } } -// getAddress 解析和设置地址信息 -func (c *Common) getAddress(parsedURL *url.URL) { - // 解析隧道地址 - tunnelAddr := parsedURL.Host - - // 解析隧道TCP地址 - if tunnelTCPAddr, err := net.ResolveTCPAddr("tcp", tunnelAddr); err == nil { - c.tunnelTCPAddr = tunnelTCPAddr - } else { - c.logger.Error("getAddress: resolveTCPAddr failed: %v", err) - } - - // 解析隧道UDP地址 - if tunnelUDPAddr, err := net.ResolveUDPAddr("udp", tunnelAddr); err == nil { - c.tunnelUDPAddr = tunnelUDPAddr - } else { - c.logger.Error("getAddress: resolveUDPAddr failed: %v", err) - } - - // 处理目标地址 - targetAddr := strings.TrimPrefix(parsedURL.Path, "/") - - // 解析目标TCP地址 - if targetTCPAddr, err := net.ResolveTCPAddr("tcp", targetAddr); err == nil { - c.targetTCPAddr = targetTCPAddr - } else { - c.logger.Error("getAddress: resolveTCPAddr failed: %v", err) - } - - // 解析目标UDP地址 - if targetUDPAddr, err := net.ResolveUDPAddr("udp", targetAddr); err == nil { - c.targetUDPAddr = targetUDPAddr - } else { - c.logger.Error("getAddress: resolveUDPAddr failed: %v", err) - } -} - // getPoolCapacity 获取连接池容量设置 func (c *Common) getPoolCapacity(parsedURL *url.URL) { if min := parsedURL.Query().Get("min"); min != "" { @@ -221,7 +253,7 @@ func (c *Common) getPoolCapacity(parsedURL *url.URL) { c.minPoolCapacity = value } } else { - c.minPoolCapacity = 64 + c.minPoolCapacity = defaultMinPool } if max := parsedURL.Query().Get("max"); max != "" { @@ -229,7 +261,16 @@ func (c *Common) getPoolCapacity(parsedURL *url.URL) { c.maxPoolCapacity = value } } else { - c.maxPoolCapacity = 1024 + c.maxPoolCapacity = defaultMaxPool + } +} + +// getRunMode 获取运行模式 +func (c *Common) getRunMode(parsedURL *url.URL) { + if mode := parsedURL.Query().Get("mode"); mode != "" { + c.runMode = mode + } else { + c.runMode = defaultRunMode } } @@ -240,16 +281,7 @@ func (c *Common) getReadTimeout(parsedURL *url.URL) { c.readTimeout = value } } else { - c.readTimeout = 1 * time.Hour - } -} - -// getRunMode 获取运行模式 -func (c *Common) getRunMode(parsedURL *url.URL) { - if mode := parsedURL.Query().Get("mode"); mode != "" { - c.runMode = mode - } else { - c.runMode = "0" + c.readTimeout = defaultReadTimeout } } @@ -260,7 +292,7 @@ func (c *Common) getRateLimit(parsedURL *url.URL) { c.rateLimit = value * 125000 } } else { - c.rateLimit = 0 + c.rateLimit = defaultRateLimit } } @@ -271,7 +303,7 @@ func (c *Common) getSlotLimit(parsedURL *url.URL) { c.slotLimit = int32(value) } } else { - c.slotLimit = 65536 + c.slotLimit = defaultSlotLimit } } @@ -280,20 +312,25 @@ func (c *Common) getProxyProtocol(parsedURL *url.URL) { if protocol := parsedURL.Query().Get("proxy"); protocol != "" { c.proxyProtocol = protocol } else { - c.proxyProtocol = "0" + c.proxyProtocol = defaultProxyProtocol } } // initConfig 初始化配置 -func (c *Common) initConfig(parsedURL *url.URL) { +func (c *Common) initConfig(parsedURL *url.URL) error { + if err := c.getAddress(parsedURL); err != nil { + return err + } + c.getTunnelKey(parsedURL) - c.getAddress(parsedURL) c.getPoolCapacity(parsedURL) - c.getReadTimeout(parsedURL) c.getRunMode(parsedURL) + c.getReadTimeout(parsedURL) c.getRateLimit(parsedURL) c.getSlotLimit(parsedURL) c.getProxyProtocol(parsedURL) + + return nil } // sendProxyV1Header 发送PROXY v1 @@ -517,7 +554,19 @@ func (c *Common) commonQueue() error { if err != nil { return fmt.Errorf("commonQueue: readBytes failed: %w", err) } - signal := string(c.xor(bytes.TrimSuffix(rawSignal, []byte{'\n'}))) + + // 解码信号 + signalData, err := c.decode(rawSignal) + if err != nil { + c.logger.Error("commonQueue: decode signal failed: %v", err) + select { + case <-c.ctx.Done(): + return fmt.Errorf("commonQueue: context error: %w", c.ctx.Err()) + case <-time.After(50 * time.Millisecond): + } + continue + } + signal := string(signalData) // 将信号发送到通道 select { @@ -550,7 +599,7 @@ func (c *Common) healthCheck() error { // 连接池健康度检查 if c.tunnelPool.ErrorCount() > c.tunnelPool.Active()/2 { // 发送刷新信号到对端 - _, err := c.tunnelTCPConn.Write(append(c.xor([]byte(flushURL.String())), '\n')) + _, err := c.tunnelTCPConn.Write(c.encode([]byte(flushURL.String()))) if err != nil { c.mu.Unlock() return fmt.Errorf("healthCheck: write flush signal failed: %w", err) @@ -569,7 +618,7 @@ func (c *Common) healthCheck() error { // 发送PING信号 c.checkPoint = time.Now() - _, err := c.tunnelTCPConn.Write(append(c.xor([]byte(pingURL.String())), '\n')) + _, err := c.tunnelTCPConn.Write(c.encode([]byte(pingURL.String()))) if err != nil { c.mu.Unlock() return fmt.Errorf("healthCheck: write ping signal failed: %w", err) @@ -674,7 +723,7 @@ func (c *Common) commonTCPLoop() { } c.mu.Lock() - _, err = c.tunnelTCPConn.Write(append(c.xor([]byte(launchURL.String())), '\n')) + _, err = c.tunnelTCPConn.Write(c.encode([]byte(launchURL.String()))) c.mu.Unlock() if err != nil { @@ -684,9 +733,16 @@ func (c *Common) commonTCPLoop() { c.logger.Debug("TCP launch signal: cid %v -> %v", id, c.tunnelTCPConn.RemoteAddr()) + buffer1 := c.getTCPBuffer() + buffer2 := c.getTCPBuffer() + defer func() { + c.putTCPBuffer(buffer1) + c.putTCPBuffer(buffer2) + }() + // 交换数据 c.logger.Debug("Starting exchange: %v <-> %v", remoteConn.LocalAddr(), targetConn.LocalAddr()) - c.logger.Debug("Exchange complete: %v", conn.DataExchange(remoteConn, targetConn, c.readTimeout)) + c.logger.Debug("Exchange complete: %v", conn.DataExchange(remoteConn, targetConn, c.readTimeout, buffer1, buffer2)) }(targetConn) } } @@ -698,17 +754,17 @@ func (c *Common) commonUDPLoop() { return } - buffer := getUDPBuffer() + buffer := c.getUDPBuffer() // 读取来自目标的UDP数据 x, clientAddr, err := c.targetUDPConn.ReadFromUDP(buffer) if err != nil { if c.ctx.Err() != nil || err == net.ErrClosed { - putUDPBuffer(buffer) + c.putUDPBuffer(buffer) return } c.logger.Error("commonUDPLoop: readFromUDP failed: %v", err) - putUDPBuffer(buffer) + c.putUDPBuffer(buffer) select { case <-c.ctx.Done(): @@ -733,7 +789,7 @@ func (c *Common) commonUDPLoop() { // 尝试获取UDP连接槽位 if !c.tryAcquireSlot(true) { c.logger.Error("commonUDPLoop: UDP slot limit reached: %v/%v", c.udpSlot, c.slotLimit) - putUDPBuffer(buffer) + c.putUDPBuffer(buffer) continue } @@ -760,8 +816,8 @@ func (c *Common) commonUDPLoop() { c.releaseSlot(true) }() - buffer := getUDPBuffer() - defer putUDPBuffer(buffer) + buffer := c.getUDPBuffer() + defer c.putUDPBuffer(buffer) reader := &conn.TimeoutReader{Conn: remoteConn, Timeout: c.readTimeout} for { @@ -800,7 +856,7 @@ func (c *Common) commonUDPLoop() { } c.mu.Lock() - _, err = c.tunnelTCPConn.Write(append(c.xor([]byte(launchURL.String())), '\n')) + _, err = c.tunnelTCPConn.Write(c.encode([]byte(launchURL.String()))) c.mu.Unlock() if err != nil { c.logger.Error("commonUDPLoop: write launch signal failed: %v", err) @@ -817,13 +873,13 @@ func (c *Common) commonUDPLoop() { c.logger.Error("commonUDPLoop: write to tunnel failed: %v", err) c.targetUDPSession.Delete(sessionKey) remoteConn.Close() - putUDPBuffer(buffer) + c.putUDPBuffer(buffer) continue } // 传输完成 c.logger.Debug("Transfer complete: %v <-> %v", remoteConn.LocalAddr(), c.targetUDPConn.LocalAddr()) - putUDPBuffer(buffer) + c.putUDPBuffer(buffer) } } @@ -878,7 +934,7 @@ func (c *Common) commonOnce() error { go c.commonUDPOnce(signalURL) case "i": // PING c.mu.Lock() - _, err := c.tunnelTCPConn.Write(append(c.xor([]byte(pongURL.String())), '\n')) + _, err := c.tunnelTCPConn.Write(c.encode([]byte(pongURL.String()))) c.mu.Unlock() if err != nil { return fmt.Errorf("commonOnce: write pong signal failed: %w", err) @@ -954,9 +1010,16 @@ func (c *Common) commonTCPOnce(signalURL *url.URL) { return } + buffer1 := c.getTCPBuffer() + buffer2 := c.getTCPBuffer() + defer func() { + c.putTCPBuffer(buffer1) + c.putTCPBuffer(buffer2) + }() + // 交换数据 c.logger.Debug("Starting exchange: %v <-> %v", remoteConn.LocalAddr(), targetConn.LocalAddr()) - c.logger.Debug("Exchange complete: %v", conn.DataExchange(remoteConn, targetConn, c.readTimeout)) + c.logger.Debug("Exchange complete: %v", conn.DataExchange(remoteConn, targetConn, c.readTimeout, buffer1, buffer2)) } // commonUDPOnce 共用处理单个UDP请求 @@ -1013,8 +1076,8 @@ func (c *Common) commonUDPOnce(signalURL *url.URL) { go func() { defer func() { done <- struct{}{} }() - buffer := getUDPBuffer() - defer putUDPBuffer(buffer) + buffer := c.getUDPBuffer() + defer c.putUDPBuffer(buffer) reader := &conn.TimeoutReader{Conn: remoteConn, Timeout: c.readTimeout} for { if c.ctx.Err() != nil { @@ -1047,8 +1110,8 @@ func (c *Common) commonUDPOnce(signalURL *url.URL) { go func() { defer func() { done <- struct{}{} }() - buffer := getUDPBuffer() - defer putUDPBuffer(buffer) + buffer := c.getUDPBuffer() + defer c.putUDPBuffer(buffer) reader := &conn.TimeoutReader{Conn: targetConn, Timeout: c.readTimeout} for { if c.ctx.Err() != nil { @@ -1204,10 +1267,16 @@ func (c *Common) singleTCPLoop() error { c.logger.Error("singleTCPLoop: sendProxyV1Header failed: %v", err) return } + buffer1 := c.getTCPBuffer() + buffer2 := c.getTCPBuffer() + defer func() { + c.putTCPBuffer(buffer1) + c.putTCPBuffer(buffer2) + }() // 交换数据 c.logger.Debug("Starting exchange: %v <-> %v", tunnelConn.LocalAddr(), targetConn.LocalAddr()) - c.logger.Debug("Exchange complete: %v", conn.DataExchange(tunnelConn, targetConn, c.readTimeout)) + c.logger.Debug("Exchange complete: %v", conn.DataExchange(tunnelConn, targetConn, c.readTimeout, buffer1, buffer2)) }(tunnelConn) } } @@ -1219,18 +1288,18 @@ func (c *Common) singleUDPLoop() error { return fmt.Errorf("singleUDPLoop: context error: %w", c.ctx.Err()) } - buffer := getUDPBuffer() + buffer := c.getUDPBuffer() // 读取来自隧道的UDP数据 x, clientAddr, err := c.tunnelUDPConn.ReadFromUDP(buffer) if err != nil { if c.ctx.Err() != nil || err == net.ErrClosed { - putUDPBuffer(buffer) + c.putUDPBuffer(buffer) return fmt.Errorf("singleUDPLoop: context error: %w", c.ctx.Err()) } c.logger.Error("singleUDPLoop: ReadFromUDP failed: %v", err) - putUDPBuffer(buffer) + c.putUDPBuffer(buffer) select { case <-c.ctx.Done(): return fmt.Errorf("singleUDPLoop: context error: %w", c.ctx.Err()) @@ -1253,7 +1322,7 @@ func (c *Common) singleUDPLoop() error { // 尝试获取UDP连接槽位 if !c.tryAcquireSlot(true) { c.logger.Error("singleUDPLoop: UDP slot limit reached: %v/%v", c.udpSlot, c.slotLimit) - putUDPBuffer(buffer) + c.putUDPBuffer(buffer) continue } @@ -1262,7 +1331,7 @@ func (c *Common) singleUDPLoop() error { if err != nil { c.logger.Error("singleUDPLoop: dialTimeout failed: %v", err) c.releaseSlot(true) - putUDPBuffer(buffer) + c.putUDPBuffer(buffer) continue } targetConn = newSession @@ -1277,8 +1346,8 @@ func (c *Common) singleUDPLoop() error { c.releaseSlot(true) }() - buffer := getUDPBuffer() - defer putUDPBuffer(buffer) + buffer := c.getUDPBuffer() + defer c.putUDPBuffer(buffer) reader := &conn.TimeoutReader{Conn: targetConn, Timeout: c.readTimeout} for { @@ -1326,12 +1395,12 @@ func (c *Common) singleUDPLoop() error { if targetConn != nil { targetConn.Close() } - putUDPBuffer(buffer) + c.putUDPBuffer(buffer) return fmt.Errorf("singleUDPLoop: write to target failed: %w", err) } // 传输完成 c.logger.Debug("Transfer complete: %v <-> %v", targetConn.LocalAddr(), c.tunnelUDPConn.LocalAddr()) - putUDPBuffer(buffer) + c.putUDPBuffer(buffer) } } diff --git a/nodepass/internal/master.go b/nodepass/internal/master.go index ada1790d7a..4dda79b377 100644 --- a/nodepass/internal/master.go +++ b/nodepass/internal/master.go @@ -38,6 +38,9 @@ const ( apiKeyID = "********" // API Key的特殊ID tcpingSemLimit = 10 // TCPing最大并发数 baseDuration = 100 * time.Millisecond // 基准持续时间 + maxTagsCount = 50 // 最大标签数量 + maxTagKeyLen = 100 // 标签键最大长度 + maxTagValueLen = 500 // 标签值最大长度 ) // Swagger UI HTML模板 @@ -64,6 +67,7 @@ const swaggerUIHTML = ` // Master 实现主控模式功能 type Master struct { Common // 继承通用功能 + alias string // 主控别名 prefix string // API前缀 version string // NP版本 hostname string // 隧道名称 @@ -80,7 +84,7 @@ type Master struct { notifyChannel chan *InstanceEvent // 事件通知通道 tcpingSem chan struct{} // TCPing并发控制 startTime time.Time // 启动时间 - backupDone chan struct{} // 备份停止信号 + periodicDone chan struct{} // 定期任务停止信号 } // Instance 实例信息 @@ -90,7 +94,9 @@ type Instance struct { Type string `json:"type"` // 实例类型 Status string `json:"status"` // 实例状态 URL string `json:"url"` // 实例URL + Config string `json:"config"` // 实例配置 Restart bool `json:"restart"` // 是否自启动 + Tags []Tag `json:"tags"` // 标签数组 Mode int32 `json:"mode"` // 实例模式 Ping int32 `json:"ping"` // 端内延迟 Pool int32 `json:"pool"` // 池连接数 @@ -106,25 +112,32 @@ type Instance struct { UDPTXBase uint64 `json:"-" gob:"-"` // UDP发送字节数基线(不序列化) cmd *exec.Cmd `json:"-" gob:"-"` // 命令对象(不序列化) stopped chan struct{} `json:"-" gob:"-"` // 停止信号通道(不序列化) + deleted bool `json:"-" gob:"-"` // 删除标志(不序列化) cancelFunc context.CancelFunc `json:"-" gob:"-"` // 取消函数(不序列化) lastCheckPoint time.Time `json:"-" gob:"-"` // 上次检查点时间(不序列化) } +// Tag 标签结构体 +type Tag struct { + Key string `json:"key"` // 标签键 + Value string `json:"value"` // 标签值 +} + // InstanceEvent 实例事件信息 type InstanceEvent struct { - Type string `json:"type"` // 事件类型:initial, create, update, delete, shutdown, log - Time time.Time `json:"time"` // 事件时间 - Instance *Instance `json:"instance"` // 关联的实例 - Logs string `json:"logs,omitempty"` // 日志内容,仅当Type为log时有效 + Type string `json:"type"` // 事件类型:initial, create, update, delete, shutdown, log + Time time.Time `json:"time"` // 事件时间 + Instance *Instance `json:"instance"` // 关联的实例 + Logs string `json:"logs"` // 日志内容 } // SystemInfo 系统信息结构体 type SystemInfo struct { CPU int `json:"cpu"` // CPU使用率 (%) MemTotal uint64 `json:"mem_total"` // 内存容量字节数 - MemFree uint64 `json:"mem_free"` // 内存可用字节数 + MemUsed uint64 `json:"mem_used"` // 内存已用字节数 SwapTotal uint64 `json:"swap_total"` // 交换区容量字节数 - SwapFree uint64 `json:"swap_free"` // 交换区可用字节数 + SwapUsed uint64 `json:"swap_used"` // 交换区已用字节数 NetRX uint64 `json:"netrx"` // 网络接收字节数 NetTX uint64 `json:"nettx"` // 网络发送字节数 DiskR uint64 `json:"diskr"` // 磁盘读取字节数 @@ -191,6 +204,34 @@ func (m *Master) performTCPing(target string) *TCPingResult { return result } +// validateTags 验证标签的有效性 +func validateTags(tags []Tag) error { + if len(tags) > maxTagsCount { + return fmt.Errorf("too many tags: maximum %d allowed", maxTagsCount) + } + + keySet := make(map[string]bool) + for _, tag := range tags { + if len(tag.Key) == 0 { + return fmt.Errorf("tag key cannot be empty") + } + if len(tag.Key) > maxTagKeyLen { + return fmt.Errorf("tag key exceeds maximum length %d", maxTagKeyLen) + } + if len(tag.Value) > maxTagValueLen { + return fmt.Errorf("tag value for key exceeds maximum length %d", maxTagValueLen) + } + + // 检查重复的键 + if keySet[tag.Key] { + return fmt.Errorf("duplicate tag key: '%s'", tag.Key) + } + keySet[tag.Key] = true + } + + return nil +} + // InstanceLogWriter 实例日志写入器 type InstanceLogWriter struct { instanceID string // 实例ID @@ -246,9 +287,11 @@ func (w *InstanceLogWriter) Write(p []byte) (n int, err error) { } w.instance.lastCheckPoint = time.Now() - w.master.instances.Store(w.instanceID, w.instance) - // 发送检查点更新事件 - w.master.sendSSEEvent("update", w.instance) + // 仅当实例未被删除时才存储和发送更新事件 + if !w.instance.deleted { + w.master.instances.Store(w.instanceID, w.instance) + w.master.sendSSEEvent("update", w.instance) + } // 过滤检查点日志 continue } @@ -256,8 +299,10 @@ func (w *InstanceLogWriter) Write(p []byte) (n int, err error) { // 输出日志加实例ID fmt.Fprintf(w.target, "%s [%s]\n", line, w.instanceID) - // 发送日志事件 - w.master.sendSSEEvent("log", w.instance, line) + // 仅当实例未被删除时才发送日志事件 + if !w.instance.deleted { + w.master.sendSSEEvent("log", w.instance, line) + } } if err := scanner.Err(); err != nil { @@ -274,12 +319,11 @@ func setCorsHeaders(w http.ResponseWriter) { } // NewMaster 创建新的主控实例 -func NewMaster(parsedURL *url.URL, tlsCode string, tlsConfig *tls.Config, logger *logs.Logger, version string) *Master { +func NewMaster(parsedURL *url.URL, tlsCode string, tlsConfig *tls.Config, logger *logs.Logger, version string) (*Master, error) { // 解析主机地址 host, err := net.ResolveTCPAddr("tcp", parsedURL.Host) if err != nil { - logger.Error("newMaster: resolveTCPAddr failed: %v", err) - return nil + return nil, fmt.Errorf("newMaster: resolve host failed: %w", err) } // 获取隧道名称 @@ -319,7 +363,7 @@ func NewMaster(parsedURL *url.URL, tlsCode string, tlsConfig *tls.Config, logger notifyChannel: make(chan *InstanceEvent, semaphoreLimit), tcpingSem: make(chan struct{}, tcpingSemLimit), startTime: time.Now(), - backupDone: make(chan struct{}), + periodicDone: make(chan struct{}), } master.tunnelTCPAddr = host @@ -329,10 +373,7 @@ func NewMaster(parsedURL *url.URL, tlsCode string, tlsConfig *tls.Config, logger // 启动事件分发器 go master.startEventDispatcher() - // 启动定期备份 - go master.startPeriodicBackup() - - return master + return master, nil } // Run 管理主控生命周期 @@ -449,6 +490,9 @@ func (m *Master) Run() { } }() + // 启动定期任务 + go m.startPeriodicTasks() + // 处理系统信号 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) <-ctx.Done() @@ -467,53 +511,15 @@ func (m *Master) Run() { // Shutdown 关闭主控 func (m *Master) Shutdown(ctx context.Context) error { return m.shutdown(ctx, func() { - // 声明一个已关闭通道的集合,避免重复关闭 - var closedChannels sync.Map - - var wg sync.WaitGroup - - // 给所有订阅者一个关闭通知 - m.subscribers.Range(func(key, value any) bool { - subscriberChan := value.(chan *InstanceEvent) - wg.Add(1) - go func(ch chan *InstanceEvent) { - defer wg.Done() - // 非阻塞的方式发送关闭事件 - select { - case ch <- &InstanceEvent{ - Type: "shutdown", - Time: time.Now(), - }: - default: - // 不可用,忽略 - } - }(subscriberChan) - return true - }) - - // 等待所有订阅者处理完关闭事件 - time.Sleep(baseDuration) - - // 关闭所有订阅者通道 - m.subscribers.Range(func(key, value any) bool { - subscriberChan := value.(chan *InstanceEvent) - // 检查通道是否已关闭,如果没有则关闭它 - if _, loaded := closedChannels.LoadOrStore(subscriberChan, true); !loaded { - wg.Add(1) - go func(k any, ch chan *InstanceEvent) { - defer wg.Done() - close(ch) - m.subscribers.Delete(k) - }(key, subscriberChan) - } - return true - }) + // 通知并关闭SSE连接 + m.shutdownSSEConnections() // 停止所有运行中的实例 + var wg sync.WaitGroup m.instances.Range(func(key, value any) bool { instance := value.(*Instance) - // 如果实例正在运行,则停止它 - if instance.Status == "running" && instance.cmd != nil && instance.cmd.Process != nil { + // 如果实例需要停止,则停止它 + if instance.Status != "stopped" && instance.cmd != nil && instance.cmd.Process != nil { wg.Add(1) go func(inst *Instance) { defer wg.Done() @@ -525,8 +531,8 @@ func (m *Master) Shutdown(ctx context.Context) error { wg.Wait() - // 关闭定期备份 - close(m.backupDone) + // 关闭定期任务 + close(m.periodicDone) // 关闭事件通知通道,停止事件分发器 close(m.notifyChannel) @@ -545,6 +551,107 @@ func (m *Master) Shutdown(ctx context.Context) error { }) } +// startPeriodicTasks 启动所有定期任务 +func (m *Master) startPeriodicTasks() { + go m.startPeriodicBackup() + go m.startPeriodicCleanup() + go m.startPeriodicRestart() +} + +// startPeriodicBackup 启动定期备份 +func (m *Master) startPeriodicBackup() { + for { + select { + case <-time.After(ReloadInterval): + // 固定备份文件名 + backupPath := fmt.Sprintf("%s.backup", m.statePath) + + if err := m.saveStateToPath(backupPath); err != nil { + m.logger.Error("startPeriodicBackup: backup state failed: %v", err) + } else { + m.logger.Info("State backup saved: %v", backupPath) + } + case <-m.periodicDone: + return + } + } +} + +// startPeriodicCleanup 启动定期清理重复ID的实例 +func (m *Master) startPeriodicCleanup() { + for { + select { + case <-time.After(reportInterval): + // 收集实例并按ID分组 + idInstances := make(map[string][]*Instance) + m.instances.Range(func(key, value any) bool { + if id := key.(string); id != apiKeyID { + idInstances[id] = append(idInstances[id], value.(*Instance)) + } + return true + }) + + // 清理重复实例 + for _, instances := range idInstances { + if len(instances) <= 1 { + continue + } + + // 选择保留实例 + keepIdx := 0 + for i, inst := range instances { + if inst.Status == "running" && instances[keepIdx].Status != "running" { + keepIdx = i + } + } + + // 清理多余实例 + for i, inst := range instances { + if i == keepIdx { + continue + } + inst.deleted = true + if inst.Status != "stopped" { + m.stopInstance(inst) + } + m.instances.Delete(inst.ID) + } + } + case <-m.periodicDone: + return + } + } +} + +// startPeriodicRestart 启动定期错误实例重启 +func (m *Master) startPeriodicRestart() { + for { + select { + case <-time.After(reportInterval): + // 收集所有error状态的实例 + var errorInstances []*Instance + m.instances.Range(func(key, value any) bool { + if id := key.(string); id != apiKeyID { + instance := value.(*Instance) + if instance.Status == "error" && !instance.deleted { + errorInstances = append(errorInstances, instance) + } + } + return true + }) + + // 重启所有error状态的实例 + for _, instance := range errorInstances { + m.stopInstance(instance) + time.Sleep(baseDuration) + m.startInstance(instance) + } + case <-m.periodicDone: + return + } + } +} + // saveState 保存实例状态到文件 func (m *Master) saveState() error { return m.saveStateToPath(m.statePath) @@ -618,27 +725,15 @@ func (m *Master) saveStateToPath(filePath string) error { return nil } -// startPeriodicBackup 启动定期备份 -func (m *Master) startPeriodicBackup() { - for { - select { - case <-time.After(ReloadInterval): - // 固定备份文件名 - backupPath := fmt.Sprintf("%s.backup", m.statePath) - - if err := m.saveStateToPath(backupPath); err != nil { - m.logger.Error("startPeriodicBackup: backup state failed: %v", err) - } else { - m.logger.Info("State backup saved: %v", backupPath) - } - case <-m.backupDone: - return - } - } -} - // loadState 从文件加载实例状态 func (m *Master) loadState() { + // 清理旧的临时文件 + if tmpFiles, _ := filepath.Glob(filepath.Join(filepath.Dir(m.statePath), "np-*.tmp")); tmpFiles != nil { + for _, f := range tmpFiles { + os.Remove(f) + } + } + // 检查文件是否存在 if _, err := os.Stat(m.statePath); os.IsNotExist(err) { return @@ -663,6 +758,12 @@ func (m *Master) loadState() { // 恢复实例 for id, instance := range persistentData { instance.stopped = make(chan struct{}) + + // 生成完整配置 + if instance.Config == "" && instance.ID != apiKeyID { + instance.Config = m.generateConfigURL(instance) + } + m.instances.Store(id, instance) // 处理自启动 @@ -679,31 +780,56 @@ func (m *Master) loadState() { func (m *Master) handleOpenAPISpec(w http.ResponseWriter, r *http.Request) { setCorsHeaders(w) w.Header().Set("Content-Type", "application/json") - w.Write([]byte(generateOpenAPISpec())) + w.Write([]byte(m.generateOpenAPISpec())) } // handleSwaggerUI 处理Swagger UI请求 func (m *Master) handleSwaggerUI(w http.ResponseWriter, r *http.Request) { setCorsHeaders(w) w.Header().Set("Content-Type", "text/html") - fmt.Fprintf(w, swaggerUIHTML, generateOpenAPISpec()) + fmt.Fprintf(w, swaggerUIHTML, m.generateOpenAPISpec()) } // handleInfo 处理系统信息请求 func (m *Master) handleInfo(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - httpError(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } + switch r.Method { + case http.MethodGet: + writeJSON(w, http.StatusOK, m.getMasterInfo()) + case http.MethodPost: + var reqData struct { + Alias string `json:"alias"` + } + if err := json.NewDecoder(r.Body).Decode(&reqData); err != nil { + httpError(w, "Invalid request body", http.StatusBadRequest) + return + } + + // 更新主控别名 + if len(reqData.Alias) > maxTagKeyLen { + httpError(w, fmt.Sprintf("Master alias exceeds maximum length %d", maxTagKeyLen), http.StatusBadRequest) + return + } + m.alias = reqData.Alias + + writeJSON(w, http.StatusOK, m.getMasterInfo()) + + default: + httpError(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +// getMasterInfo 获取完整的主控信息 +func (m *Master) getMasterInfo() map[string]any { info := map[string]any{ + "alias": m.alias, "os": runtime.GOOS, "arch": runtime.GOARCH, "cpu": -1, "mem_total": uint64(0), - "mem_free": uint64(0), + "mem_used": uint64(0), "swap_total": uint64(0), - "swap_free": uint64(0), + "swap_used": uint64(0), "netrx": uint64(0), "nettx": uint64(0), "diskr": uint64(0), @@ -722,9 +848,9 @@ func (m *Master) handleInfo(w http.ResponseWriter, r *http.Request) { sysInfo := getLinuxSysInfo() info["cpu"] = sysInfo.CPU info["mem_total"] = sysInfo.MemTotal - info["mem_free"] = sysInfo.MemFree + info["mem_used"] = sysInfo.MemUsed info["swap_total"] = sysInfo.SwapTotal - info["swap_free"] = sysInfo.SwapFree + info["swap_used"] = sysInfo.SwapUsed info["netrx"] = sysInfo.NetRX info["nettx"] = sysInfo.NetTX info["diskr"] = sysInfo.DiskR @@ -732,7 +858,7 @@ func (m *Master) handleInfo(w http.ResponseWriter, r *http.Request) { info["sysup"] = sysInfo.SysUp } - writeJSON(w, http.StatusOK, info) + return info } // getLinuxSysInfo 获取Linux系统信息 @@ -740,9 +866,9 @@ func getLinuxSysInfo() SystemInfo { info := SystemInfo{ CPU: -1, MemTotal: 0, - MemFree: 0, + MemUsed: 0, SwapTotal: 0, - SwapFree: 0, + SwapUsed: 0, NetRX: 0, NetTX: 0, DiskR: 0, @@ -754,7 +880,7 @@ func getLinuxSysInfo() SystemInfo { return info } - // CPU使用率:解析/proc/stat + // CPU占用:解析/proc/stat readStat := func() (idle, total uint64) { data, err := os.ReadFile("/proc/stat") if err != nil { @@ -783,9 +909,9 @@ func getLinuxSysInfo() SystemInfo { info.CPU = min(int((deltaTotal-deltaIdle)*100/deltaTotal/uint64(numCPU)), 100) } - // RAM使用率:解析/proc/meminfo + // RAM占用:解析/proc/meminfo if data, err := os.ReadFile("/proc/meminfo"); err == nil { - var memTotal, memFree, swapTotal, swapFree uint64 + var memTotal, memAvailable, swapTotal, swapFree uint64 for line := range strings.SplitSeq(string(data), "\n") { if fields := strings.Fields(line); len(fields) >= 2 { if val, err := strconv.ParseUint(fields[1], 10, 64); err == nil { @@ -793,8 +919,8 @@ func getLinuxSysInfo() SystemInfo { switch fields[0] { case "MemTotal:": memTotal = val - case "MemFree:": - memFree = val + case "MemAvailable:": + memAvailable = val case "SwapTotal:": swapTotal = val case "SwapFree:": @@ -804,9 +930,9 @@ func getLinuxSysInfo() SystemInfo { } } info.MemTotal = memTotal - info.MemFree = memFree + info.MemUsed = memTotal - memAvailable info.SwapTotal = swapTotal - info.SwapFree = swapFree + info.SwapUsed = swapTotal - swapFree } // 网络I/O:解析/proc/net/dev @@ -914,9 +1040,12 @@ func (m *Master) handleInstances(w http.ResponseWriter, r *http.Request) { Type: instanceType, URL: m.enhanceURL(reqData.URL, instanceType), Status: "stopped", - Restart: false, + Restart: true, + Tags: []Tag{}, stopped: make(chan struct{}), } + + instance.Config = m.generateConfigURL(instance) m.instances.Store(id, instance) // 启动实例 @@ -978,6 +1107,7 @@ func (m *Master) handlePatchInstance(w http.ResponseWriter, r *http.Request, id Alias string `json:"alias,omitempty"` Action string `json:"action,omitempty"` Restart *bool `json:"restart,omitempty"` + Tags []Tag `json:"tags,omitempty"` } if err := json.NewDecoder(r.Body).Decode(&reqData); err == nil { if id == apiKeyID { @@ -988,6 +1118,44 @@ func (m *Master) handlePatchInstance(w http.ResponseWriter, r *http.Request, id m.sendSSEEvent("update", instance) } } else { + // 处理标签更新 + if reqData.Tags != nil { + if err := validateTags(reqData.Tags); err != nil { + httpError(w, err.Error(), http.StatusBadRequest) + return + } + + // 创建现有标签的映射表 + existingTags := make(map[string]Tag) + for _, tag := range instance.Tags { + existingTags[tag.Key] = tag + } + + for _, tag := range reqData.Tags { + if tag.Value == "" { + // value为空,删除key + delete(existingTags, tag.Key) + } else { + // value非空,更新或添加key + existingTags[tag.Key] = tag + } + } + + // 将映射表转换回标签数组 + newTags := make([]Tag, 0, len(existingTags)) + for _, tag := range existingTags { + newTags = append(newTags, tag) + } + + instance.Tags = newTags + m.instances.Store(id, instance) + go m.saveState() + m.logger.Info("Tags updated: [%v]", instance.ID) + + // 发送标签变更事件 + m.sendSSEEvent("update", instance) + } + // 重置流量统计 if reqData.Action == "reset" { instance.TCPRX = 0 @@ -1015,6 +1183,10 @@ func (m *Master) handlePatchInstance(w http.ResponseWriter, r *http.Request, id // 更新实例别名 if reqData.Alias != "" && instance.Alias != reqData.Alias { + if len(reqData.Alias) > maxTagKeyLen { + httpError(w, fmt.Sprintf("Instance alias exceeds maximum length %d", maxTagKeyLen), http.StatusBadRequest) + return + } instance.Alias = reqData.Alias m.instances.Store(id, instance) go m.saveState() @@ -1072,8 +1244,8 @@ func (m *Master) handlePutInstance(w http.ResponseWriter, r *http.Request, id st return } - // 如果实例正在运行,先停止它 - if instance.Status == "running" { + // 如果实例需要停止,先停止它 + if instance.Status != "stopped" { m.stopInstance(instance) time.Sleep(baseDuration) } @@ -1081,6 +1253,7 @@ func (m *Master) handlePutInstance(w http.ResponseWriter, r *http.Request, id st // 更新实例URL和类型 instance.URL = enhancedURL instance.Type = instanceType + instance.Config = m.generateConfigURL(instance) // 更新实例状态 instance.Status = "stopped" @@ -1103,8 +1276,9 @@ func (m *Master) handlePutInstance(w http.ResponseWriter, r *http.Request, id st func (m *Master) regenerateAPIKey(instance *Instance) { instance.URL = generateAPIKey() m.instances.Store(apiKeyID, instance) - go m.saveState() m.logger.Info("API Key regenerated: %v", instance.URL) + go m.saveState() + go m.shutdownSSEConnections() } // processInstanceAction 处理实例操作 @@ -1115,19 +1289,15 @@ func (m *Master) processInstanceAction(instance *Instance, action string) { go m.startInstance(instance) } case "stop": - if instance.Status == "running" { + if instance.Status != "stopped" { go m.stopInstance(instance) } case "restart": - if instance.Status == "running" { - go func() { - m.stopInstance(instance) - time.Sleep(baseDuration) - m.startInstance(instance) - }() - } else { - go m.startInstance(instance) - } + go func() { + m.stopInstance(instance) + time.Sleep(baseDuration) + m.startInstance(instance) + }() } } @@ -1139,7 +1309,11 @@ func (m *Master) handleDeleteInstance(w http.ResponseWriter, id string, instance return } - if instance.Status == "running" { + // 标记实例为已删除 + instance.deleted = true + m.instances.Store(id, instance) + + if instance.Status != "stopped" { m.stopInstance(instance) } m.instances.Delete(id) @@ -1202,12 +1376,14 @@ func (m *Master) handleSSE(w http.ResponseWriter, r *http.Request) { // 客户端连接关闭标志 connectionClosed := make(chan struct{}) - // 监听客户端连接是否关闭,但不关闭通道,留给Shutdown处理 + // 监听客户端连接是否关闭 go func() { <-ctx.Done() close(connectionClosed) - // 只从映射表中移除,但不关闭通道 - m.subscribers.Delete(subscriberID) + // 从映射表中移除并关闭通道 + if ch, exists := m.subscribers.LoadAndDelete(subscriberID); exists { + close(ch.(chan *InstanceEvent)) + } }() // 持续发送事件到客户端 @@ -1255,6 +1431,32 @@ func (m *Master) sendSSEEvent(eventType string, instance *Instance, logs ...stri } } +// shutdownSSEConnections 通知并关闭SSE连接 +func (m *Master) shutdownSSEConnections() { + var wg sync.WaitGroup + + // 发送shutdown通知并关闭通道 + m.subscribers.Range(func(key, value any) bool { + ch := value.(chan *InstanceEvent) + wg.Add(1) + go func(subscriberID any, eventChan chan *InstanceEvent) { + defer wg.Done() + // 发送shutdown通知 + select { + case eventChan <- &InstanceEvent{Type: "shutdown", Time: time.Now()}: + default: + } + // 从映射表中移除并关闭通道 + if _, exists := m.subscribers.LoadAndDelete(subscriberID); exists { + close(eventChan) + } + }(key, ch) + return true + }) + + wg.Wait() +} + // startEventDispatcher 启动事件分发器 func (m *Master) startEventDispatcher() { for event := range m.notifyChannel { @@ -1345,9 +1547,7 @@ func (m *Master) startInstance(instance *Instance) { // monitorInstance 监控实例状态 func (m *Master) monitorInstance(instance *Instance, cmd *exec.Cmd) { done := make(chan error, 1) - go func() { - done <- cmd.Wait() - }() + go func() { done <- cmd.Wait() }() for { select { @@ -1371,7 +1571,7 @@ func (m *Master) monitorInstance(instance *Instance, cmd *exec.Cmd) { } return case <-time.After(reportInterval): - if !instance.lastCheckPoint.IsZero() && time.Since(instance.lastCheckPoint) > 5*reportInterval { + if !instance.lastCheckPoint.IsZero() && time.Since(instance.lastCheckPoint) > 3*reportInterval { instance.Status = "error" m.instances.Store(instance.ID, instance) m.sendSSEEvent("update", instance) @@ -1469,6 +1669,86 @@ func (m *Master) enhanceURL(instanceURL string, instanceType string) string { return parsedURL.String() } +// generateConfigURL 生成实例的完整URL +func (m *Master) generateConfigURL(instance *Instance) string { + parsedURL, err := url.Parse(instance.URL) + if err != nil { + m.logger.Error("generateConfigURL: invalid URL format: %v", err) + return instance.URL + } + + query := parsedURL.Query() + + // 设置日志级别 + if m.logLevel != "" && query.Get("log") == "" { + query.Set("log", m.logLevel) + } + + // 设置TLS配置 + if instance.Type == "server" && m.tlsCode != "0" { + if query.Get("tls") == "" { + query.Set("tls", m.tlsCode) + } + + // 为TLS code-2设置证书和密钥 + if m.tlsCode == "2" { + if m.crtPath != "" && query.Get("crt") == "" { + query.Set("crt", m.crtPath) + } + if m.keyPath != "" && query.Get("key") == "" { + query.Set("key", m.keyPath) + } + } + } + + // 根据实例类型设置默认参数 + switch instance.Type { + case "client": + // client参数: min, mode, read, rate, slot, proxy + if query.Get("min") == "" { + query.Set("min", strconv.Itoa(defaultMinPool)) + } + if query.Get("mode") == "" { + query.Set("mode", defaultRunMode) + } + if query.Get("read") == "" { + query.Set("read", defaultReadTimeout.String()) + } + if query.Get("rate") == "" { + query.Set("rate", strconv.Itoa(defaultRateLimit)) + } + if query.Get("slot") == "" { + query.Set("slot", strconv.Itoa(defaultSlotLimit)) + } + if query.Get("proxy") == "" { + query.Set("proxy", defaultProxyProtocol) + } + case "server": + // server参数: max, mode, read, rate, slot, proxy + if query.Get("max") == "" { + query.Set("max", strconv.Itoa(defaultMaxPool)) + } + if query.Get("mode") == "" { + query.Set("mode", defaultRunMode) + } + if query.Get("read") == "" { + query.Set("read", defaultReadTimeout.String()) + } + if query.Get("rate") == "" { + query.Set("rate", strconv.Itoa(defaultRateLimit)) + } + if query.Get("slot") == "" { + query.Set("slot", strconv.Itoa(defaultSlotLimit)) + } + if query.Get("proxy") == "" { + query.Set("proxy", defaultProxyProtocol) + } + } + + parsedURL.RawQuery = query.Encode() + return parsedURL.String() +} + // generateID 生成随机ID func generateID() string { bytes := make([]byte, 4) @@ -1500,7 +1780,7 @@ func writeJSON(w http.ResponseWriter, statusCode int, data any) { } // generateOpenAPISpec 生成OpenAPI规范文档 -func generateOpenAPISpec() string { +func (m *Master) generateOpenAPISpec() string { return fmt.Sprintf(`{ "openapi": "3.1.1", "info": { @@ -1508,7 +1788,7 @@ func generateOpenAPISpec() string { "description": "API for managing NodePass server and client instances", "version": "%s" }, - "servers": [{"url": "/{prefix}/v1", "variables": {"prefix": {"default": "api", "description": "API prefix path"}}}], + "servers": [{"url": "%s"}], "security": [{"ApiKeyAuth": []}], "paths": { "/instances": { @@ -1606,6 +1886,17 @@ func generateOpenAPISpec() string { "401": {"description": "Unauthorized"}, "405": {"description": "Method not allowed"} } + }, + "post": { + "summary": "Update master alias", + "security": [{"ApiKeyAuth": []}], + "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UpdateMasterAliasRequest"}}}}, + "responses": { + "200": {"description": "Success", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MasterInfo"}}}}, + "400": {"description": "Invalid input"}, + "401": {"description": "Unauthorized"}, + "405": {"description": "Method not allowed"} + } } }, "/tcping": { @@ -1664,7 +1955,9 @@ func generateOpenAPISpec() string { "type": {"type": "string", "enum": ["client", "server"], "description": "Type of instance"}, "status": {"type": "string", "enum": ["running", "stopped", "error"], "description": "Instance status"}, "url": {"type": "string", "description": "Command string or API Key"}, + "config": {"type": "string", "description": "Instance configuration URL"}, "restart": {"type": "boolean", "description": "Restart policy"}, + "tags": {"type": "array", "items": {"$ref": "#/components/schemas/Tag"}, "description": "Tag array"}, "mode": {"type": "integer", "description": "Instance mode"}, "ping": {"type": "integer", "description": "TCPing latency"}, "pool": {"type": "integer", "description": "Pool active count"}, @@ -1686,7 +1979,8 @@ func generateOpenAPISpec() string { "properties": { "alias": {"type": "string", "description": "Instance alias"}, "action": {"type": "string", "enum": ["start", "stop", "restart", "reset"], "description": "Action for the instance"}, - "restart": {"type": "boolean", "description": "Instance restart policy"} + "restart": {"type": "boolean", "description": "Instance restart policy"}, + "tags": {"type": "array", "items": {"$ref": "#/components/schemas/Tag"}, "description": "Tag array"} } }, "PutInstanceRequest": { @@ -1697,13 +1991,14 @@ func generateOpenAPISpec() string { "MasterInfo": { "type": "object", "properties": { + "alias": {"type": "string", "description": "Master alias"}, "os": {"type": "string", "description": "Operating system"}, "arch": {"type": "string", "description": "System architecture"}, "cpu": {"type": "integer", "description": "CPU usage percentage"}, "mem_total": {"type": "integer", "format": "int64", "description": "Total memory in bytes"}, - "mem_free": {"type": "integer", "format": "int64", "description": "Free memory in bytes"}, + "mem_used": {"type": "integer", "format": "int64", "description": "Used memory in bytes"}, "swap_total": {"type": "integer", "format": "int64", "description": "Total swap space in bytes"}, - "swap_free": {"type": "integer", "format": "int64", "description": "Free swap space in bytes"}, + "swap_used": {"type": "integer", "format": "int64", "description": "Used swap space in bytes"}, "netrx": {"type": "integer", "format": "int64", "description": "Network received bytes"}, "nettx": {"type": "integer", "format": "int64", "description": "Network transmitted bytes"}, "diskr": {"type": "integer", "format": "int64", "description": "Disk read bytes"}, @@ -1718,6 +2013,11 @@ func generateOpenAPISpec() string { "key": {"type": "string", "description": "Private key path"} } }, + "UpdateMasterAliasRequest": { + "type": "object", + "required": ["alias"], + "properties": {"alias": {"type": "string", "description": "Master alias"}} + }, "TCPingResult": { "type": "object", "properties": { @@ -1726,8 +2026,16 @@ func generateOpenAPISpec() string { "latency": {"type": "integer", "format": "int64", "description": "Latency in milliseconds"}, "error": {"type": "string", "nullable": true, "description": "Error message"} } + }, + "Tag": { + "type": "object", + "required": ["key", "value"], + "properties": { + "key": {"type": "string", "description": "Tag key"}, + "value": {"type": "string", "description": "Tag value"} + } } } } -}`, openAPIVersion) +}`, openAPIVersion, m.prefix) } diff --git a/nodepass/internal/server.go b/nodepass/internal/server.go index 2abbc267b6..da00f86200 100644 --- a/nodepass/internal/server.go +++ b/nodepass/internal/server.go @@ -3,7 +3,6 @@ package internal import ( "bufio" - "bytes" "context" "crypto/tls" "fmt" @@ -13,6 +12,7 @@ import ( "os" "os/signal" "strconv" + "sync" "syscall" "time" @@ -29,26 +29,40 @@ type Server struct { } // NewServer 创建新的服务端实例 -func NewServer(parsedURL *url.URL, tlsCode string, tlsConfig *tls.Config, logger *logs.Logger) *Server { +func NewServer(parsedURL *url.URL, tlsCode string, tlsConfig *tls.Config, logger *logs.Logger) (*Server, error) { server := &Server{ Common: Common{ tlsCode: tlsCode, logger: logger, signalChan: make(chan string, semaphoreLimit), + tcpBufferPool: &sync.Pool{ + New: func() any { + buf := make([]byte, tcpDataBufSize) + return &buf + }, + }, + udpBufferPool: &sync.Pool{ + New: func() any { + buf := make([]byte, udpDataBufSize) + return &buf + }, + }, }, tlsConfig: tlsConfig, } - server.initConfig(parsedURL) + if err := server.initConfig(parsedURL); err != nil { + return nil, fmt.Errorf("newServer: initConfig failed: %w", err) + } server.initRateLimiter() - return server + return server, nil } // Run 管理服务端生命周期 func (s *Server) Run() { logInfo := func(prefix string) { - s.logger.Info("%v: %v@%v/%v?max=%v&mode=%v&read=%v&rate=%v&slot=%v", + s.logger.Info("%v: server://%v@%v/%v?max=%v&mode=%v&read=%v&rate=%v&slot=%v&proxy=%v", prefix, s.tunnelKey, s.tunnelTCPAddr, s.targetTCPAddr, - s.maxPoolCapacity, s.runMode, s.readTimeout, s.rateLimit/125000, s.slotLimit) + s.maxPoolCapacity, s.runMode, s.readTimeout, s.rateLimit/125000, s.slotLimit, s.proxyProtocol) } logInfo("Server started") @@ -168,7 +182,7 @@ func (s *Server) tunnelHandshake() error { tunnelTCPConn.SetReadDeadline(time.Now().Add(handshakeTimeout)) bufReader := bufio.NewReader(tunnelTCPConn) - rawTunnelKey, err := bufReader.ReadString('\n') + rawTunnelKey, err := bufReader.ReadBytes('\n') if err != nil { s.logger.Warn("tunnelHandshake: handshake timeout: %v", tunnelTCPConn.RemoteAddr()) tunnelTCPConn.Close() @@ -181,7 +195,20 @@ func (s *Server) tunnelHandshake() error { } tunnelTCPConn.SetReadDeadline(time.Time{}) - tunnelKey := string(s.xor(bytes.TrimSuffix([]byte(rawTunnelKey), []byte{'\n'}))) + + // 解码隧道密钥 + tunnelKeyData, err := s.decode(rawTunnelKey) + if err != nil { + s.logger.Warn("tunnelHandshake: decode tunnel key failed: %v", tunnelTCPConn.RemoteAddr()) + tunnelTCPConn.Close() + select { + case <-s.ctx.Done(): + return fmt.Errorf("tunnelHandshake: context error: %w", s.ctx.Err()) + case <-time.After(serviceCooldown): + } + continue + } + tunnelKey := string(tunnelKeyData) if tunnelKey != s.tunnelKey { s.logger.Warn("tunnelHandshake: access denied: %v", tunnelTCPConn.RemoteAddr()) @@ -212,7 +239,7 @@ func (s *Server) tunnelHandshake() error { Fragment: s.tlsCode, } - _, err := s.tunnelTCPConn.Write(append(s.xor([]byte(tunnelURL.String())), '\n')) + _, err := s.tunnelTCPConn.Write(s.encode([]byte(tunnelURL.String()))) if err != nil { return fmt.Errorf("tunnelHandshake: write tunnel config failed: %w", err) } diff --git a/openwrt-packages/luci-app-unblockneteasemusic/Makefile b/openwrt-packages/luci-app-unblockneteasemusic/Makefile index e9d4133163..cbb024318d 100644 --- a/openwrt-packages/luci-app-unblockneteasemusic/Makefile +++ b/openwrt-packages/luci-app-unblockneteasemusic/Makefile @@ -10,8 +10,8 @@ LUCI_DEPENDS:=+dnsmasq-full +ipset +node \ LUCI_PKGARCH:=all PKG_NAME:=luci-app-unblockneteasemusic -PKG_VERSION:=2.14 -PKG_RELEASE:=3 +PKG_VERSION:=2.15 +PKG_RELEASE:=1 PKG_MAINTAINER:=Tianling Shen diff --git a/openwrt-packages/luci-app-unblockneteasemusic/luasrc/model/cbi/unblockneteasemusic/main.lua b/openwrt-packages/luci-app-unblockneteasemusic/luasrc/model/cbi/unblockneteasemusic/main.lua index 66fb932291..1ee4d97dce 100644 --- a/openwrt-packages/luci-app-unblockneteasemusic/luasrc/model/cbi/unblockneteasemusic/main.lua +++ b/openwrt-packages/luci-app-unblockneteasemusic/luasrc/model/cbi/unblockneteasemusic/main.lua @@ -16,6 +16,7 @@ o = s:option(Value, "music_source", translate("音源接口")) o:value("default", translate("默认")) o:value("bilibili", translate("Bilibili音乐")) o:value("bilivideo", translate("Bilibili音乐(bilivideo)")) +o:value("bodian", translate("波点音乐")) o:value("joox", translate("JOOX音乐")) o:value("kugou", translate("酷狗音乐")) o:value("kuwo", translate("酷我音乐")) diff --git a/openwrt-packages/luci-app-unblockneteasemusic/root/etc/init.d/unblockneteasemusic b/openwrt-packages/luci-app-unblockneteasemusic/root/etc/init.d/unblockneteasemusic index 789fc1fe6d..8e69e2e62f 100755 --- a/openwrt-packages/luci-app-unblockneteasemusic/root/etc/init.d/unblockneteasemusic +++ b/openwrt-packages/luci-app-unblockneteasemusic/root/etc/init.d/unblockneteasemusic @@ -166,9 +166,8 @@ start_service() { ipset create "neteasemusic" hash:ip timeout 7200 config_foreach append_filter_client "acl_rule" - local netease_music_ips="$(wget -qO- "http://httpdns.n.netease.com/httpdns/v2/d?domain=music.163.com,interface.music.163.com,interface3.music.163.com,apm.music.163.com,apm3.music.163.com,clientlog.music.163.com,clientlog3.music.163.com" |jsonfilter -e '@.data.*.ip.*')" - local netease_music_ips2="$(wget -qO- "https://music.httpdns.c.163.com/d" --post-data="music.163.com,interface.music.163.com,interface3.music.163.com,apm.music.163.com,apm3.music.163.com,clientlog.music.163.com,clientlog3.music.163.com" |jsonfilter -e '@.dns.*["ips"].*')" - echo -e "${netease_music_ips}\n${netease_music_ips2}" | sort -u | awk '{print "ipset add neteasemusic "$1}' | sh + local netease_music_ips="$(wget -T10 -qO- "http://httpdns.n.netease.com/httpdns/v2/d?domain=music.163.com,interface.music.163.com,interface3.music.163.com,apm.music.163.com,apm3.music.163.com,clientlog.music.163.com,clientlog3.music.163.com" |jsonfilter -e '@.data.*.ip.*')" + echo -e "${netease_music_ips}" | sort -u | awk '{print "ipset add neteasemusic "$1}' | sh $IPT_N -N "netease_cloud_music" for local_addr in "0.0.0.0/8" "10.0.0.0/8" "127.0.0.0/8" "169.254.0.0/16" "172.16.0.0/12" "192.168.0.0/16" "224.0.0.0/4" "240.0.0.0/4"; do diff --git a/openwrt-packages/luci-app-unblockneteasemusic/root/usr/share/unblockneteasemusic/debugging.sh b/openwrt-packages/luci-app-unblockneteasemusic/root/usr/share/unblockneteasemusic/debugging.sh index a4b8098ad3..5a2af4b5e1 100755 --- a/openwrt-packages/luci-app-unblockneteasemusic/root/usr/share/unblockneteasemusic/debugging.sh +++ b/openwrt-packages/luci-app-unblockneteasemusic/root/usr/share/unblockneteasemusic/debugging.sh @@ -18,7 +18,7 @@ echo -e "uclient-fetch info:" opkg info uclient-fetch opkg info libustream-* opkg info wget-ssl -wget -O- 'https://api.github.com/repos/UnblockNeteaseMusic/server/commits?sha=enhanced&path=precompiled' | jsonfilter -e '@[0].sha' || echo -e "Failed to connect to GitHub with uclient-fetch." +wget -T10 -O- 'https://api.github.com/repos/UnblockNeteaseMusic/server/commits?sha=enhanced&path=precompiled' | jsonfilter -e '@[0].sha' || echo -e "Failed to connect to GitHub with uclient-fetch." echo -e "\n" echo -e "Node.js info:" diff --git a/openwrt-packages/luci-app-unblockneteasemusic/root/usr/share/unblockneteasemusic/update.sh b/openwrt-packages/luci-app-unblockneteasemusic/root/usr/share/unblockneteasemusic/update.sh index abebf0b18f..06f41c5211 100755 --- a/openwrt-packages/luci-app-unblockneteasemusic/root/usr/share/unblockneteasemusic/update.sh +++ b/openwrt-packages/luci-app-unblockneteasemusic/root/usr/share/unblockneteasemusic/update.sh @@ -10,22 +10,19 @@ mkdir -p "$RUN_DIR" LOCK="$RUN_DIR/update_core.lock" LOG="$RUN_DIR/run.log" -check_core_if_already_running() { - if [ -e "$LOCK" ]; then - echo -e "\nA task is already running." >> "$LOG" - exit 2 - else - touch "$LOCK" - fi -} - clean_log(){ echo "" > "$LOG" } check_core_latest_version() { - core_latest_ver="$(wget -qO- 'https://api.github.com/repos/UnblockNeteaseMusic/server/commits?sha=enhanced&path=precompiled' | jsonfilter -e '@[0].sha')" - [ -n "$core_latest_ver" ] || { echo -e "\nFailed to check latest core version, please try again later." >> "$LOG"; rm -f "$LOCK"; exit 1; } + exec 200>"$LOCK" + if ! flock -n 200 &> /dev/null; then + echo -e "\nA task is already running." >> "$LOG" + exit 2 + fi + + core_latest_ver="$(wget -T10 -qO- 'https://api.github.com/repos/UnblockNeteaseMusic/server/commits?sha=enhanced&path=precompiled' | jsonfilter -e '@[0].sha')" + [ -n "$core_latest_ver" ] || { echo -e "\nFailed to check latest core version, please try again later." >> "$LOG"; exit 1; } if [ ! -e "$UNM_DIR/core_local_ver" ]; then clean_log echo -e "Local version: NOT FOUND, latest version: $core_latest_ver." >> "$LOG" @@ -38,7 +35,6 @@ check_core_latest_version() { else echo -e "\nLocal version: $(cat $UNM_DIR/core_local_ver 2>"/dev/null"), latest version: $core_latest_ver." >> "$LOG" echo -e "You're already using the latest version." >> "$LOG" - rm -f "$LOCK" exit 3 fi fi @@ -50,22 +46,20 @@ update_core() { mkdir -p "$UNM_DIR/core" rm -rf "$UNM_DIR/core"/* - for file in $(wget -qO- "https://api.github.com/repos/UnblockNeteaseMusic/server/contents/precompiled" | jsonfilter -e '@[*].path') + for file in $(wget -T10 -qO- "https://api.github.com/repos/UnblockNeteaseMusic/server/contents/precompiled" | jsonfilter -e '@[*].path') do - wget "https://fastly.jsdelivr.net/gh/UnblockNeteaseMusic/server@$core_latest_ver/$file" -qO "$UNM_DIR/core/${file##*/}" + wget -T10 "https://fastly.jsdelivr.net/gh/UnblockNeteaseMusic/server@$core_latest_ver/$file" -qO "$UNM_DIR/core/${file##*/}" [ -s "$UNM_DIR/core/${file##*/}" ] || { echo -e "Failed to download ${file##*/}." >> "$LOG" - rm -f "$LOCK" exit 1 } done for cert in "ca.crt" "server.crt" "server.key" do - wget "https://fastly.jsdelivr.net/gh/UnblockNeteaseMusic/server@$core_latest_ver/$cert" -qO "$UNM_DIR/core/$cert" + wget -T10 "https://fastly.jsdelivr.net/gh/UnblockNeteaseMusic/server@$core_latest_ver/$cert" -qO "$UNM_DIR/core/$cert" [ -s "$UNM_DIR/core/${cert}" ] || { echo -e "Failed to download ${cert}." >> "$LOG" - rm -f "$LOCK" exit 1 } done @@ -76,17 +70,14 @@ update_core() { echo -e "Succeeded in updating core." > "$LOG" echo -e "Current core version: $core_latest_ver.\n" >> "$LOG" - rm -f "$LOCK" } case "$1" in "update_core") - check_core_if_already_running check_core_latest_version ;; "update_core_non_restart") non_restart=1 - check_core_if_already_running check_core_latest_version ;; "update_core_from_luci") diff --git a/small/luci-app-bypass/Makefile b/small/luci-app-bypass/Makefile index 98ba96401d..45666fc5a8 100644 --- a/small/luci-app-bypass/Makefile +++ b/small/luci-app-bypass/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-bypass PKG_VERSION:=1.2 -PKG_RELEASE:=21 +PKG_RELEASE:=22 PKG_CONFIG_DEPENDS:= \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server \ diff --git a/small/luci-app-bypass/root/usr/share/ucitrack/luci-app-bypass.json b/small/luci-app-bypass/root/usr/share/ucitrack/luci-app-bypass.json new file mode 100644 index 0000000000..e62d9b25ed --- /dev/null +++ b/small/luci-app-bypass/root/usr/share/ucitrack/luci-app-bypass.json @@ -0,0 +1,4 @@ +{ + "config": "bypass", + "init": "bypass" +} diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js b/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js index 191d9613a5..bcfa13b060 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js @@ -317,6 +317,37 @@ const tls_client_fingerprints = [ ['random'] ]; +const vless_encryption = { + methods: [ + ['mlkem768x25519plus', _('mlkem768x25519plus')] + ], + xormodes: [ + ['native', 'native', _('Native appearance')], + ['xorpub', 'xorpub', _('Eliminate encryption header characteristics')], + ['random', 'random', _('Randomized traffic characteristics')] + ], + tickets: [ + ['600s', '600s', _('Send random ticket of 300s-600s duration for client 0-RTT reuse.')], + ['300-600s', '300-600s', _('Send random ticket of 300s-600s duration for client 0-RTT reuse.')], + ['0s', '0s', _('1-RTT only.')] + ], + rtts: [ + ['0rtt', _('0-RTT reuse.') +' '+ _('Requires server support.')], + ['1rtt', _('1-RTT only.')] + ], + paddings: [ + ['100-111-1111', '100-111-1111: ' + _('After the 1-RTT client/server hello, padding randomly 111-1111 bytes with 100% probability.')], + ['75-0-111', '75-0-111: ' + _('Wait a random 0-111 milliseconds with 75% probability.')], + ['50-0-3333', '50-0-3333: ' + _('Send padding randomly 0-3333 bytes with 50% probability.')] + ], + keypairs: { + types: [ + ['vless-x25519', _('vless-x25519')], + ['vless-mlkem768', _('vless-mlkem768')] + ] + } +}; + const vless_flow = [ ['', _('None')], ['xtls-rprx-vision'] @@ -722,6 +753,34 @@ function decodeBase64Str(str) { ).join('')); } +function encodeBase64Str(str) { + if (!str) + return null; + + let buf = encodeURIComponent(str).split('%').slice(1).map(h => parseInt(h, 16)); + return btoa(String.fromCharCode(...buf)); +} + +function decodeBase64Bin(str) { + if (!str) + return null; + + /* Thanks to luci-app-ssr-plus */ + str = str.replace(/-/g, '+').replace(/_/g, '/'); + let padding = (4 - (str.length % 4)) % 4; + if (padding) + str = str + Array(padding + 1).join('='); + + return Array.prototype.map.call(atob(str), c => c.charCodeAt(0)); // OR Uint8Array.fromBase64(str); +} + +function encodeBase64Bin(buf) { + if (isEmpty(buf)) + return null; + + return btoa(String.fromCharCode(...buf)); // OR new Uint8Array(buf).toBase64(); +} + function generateRand(type, length) { let byteArr; if (['base64', 'hex'].includes(type)) @@ -1008,372 +1067,6 @@ function renderResDownload(section_id) { return El; } -function renderVlessEncryption(s, uciconfig) { - // ref: https://github.com/XTLS/Xray-core/pull/5067 - // https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/factory.go#L64 - // https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/server.go#L42 - -/* { - "method": "mlkem768x25519plus", - "xormode": "native", - "ticket": "600s", - "paddings": [ // Optional - "100-111-1111", - "75-0-111", - "50-0-3333", - ... - ], - "keypairs": [ - { - "type": "vless-x25519", - "private_key": "cP5Oy9MOpTaBKKE17Pfd56mbb1CIfp5EMpyBYqr2EG8", - "password": "khEcQMT8j41xWmGYKpZtQ4vd8_9VWyFVmmCDIhRJ-Uk" - }, - { - "type": "vless-mlkem768", - "seed": "UHPx3nf-FVxF95byAw0YG025aQNw9HxKej-MiG5AhTcdW_WFpHlTVYQU5NHmXP6tmljSnB2iPmSQ29fisGxEog", - "client": "h4sdZgCc5-ZefvQ8mZmReOWQdxYb0mwngMdl7pKhYEZZpGWHUPKAmxug87Bgj3GqSHs195QeVpxfrMLNB5Jm0Ge71Fc-A3aLpaS3C3pARbGQoquUDUEVDNwEWjQTvFpGTUV3Nddw_LlRmWN6Wqhguti9cpS6GhEmkBvBayFeHgZosuaQ1FMoAqIeQzSSSoguCZtGLUmdQjEs3zc5rwG1rNanbhtyI3QnooYvr3A0vggIkbmddjtjwYaVQdMAj9Moavc12EAUajOV91QA73RWVuhelbe7pLumsHiW-emIdBgVhEgDDYdGaLq1E8QjB0WbIfufnJp-CJa3Ieu9gmDASTlQBeEREeA9gfoZcTpYD8elhJIJxaPJKXchvUVkFhZarcivlKoqVuaFPzsJM7KQCBC8zfS0t_oiBka-uzg3_Hl153nMTDaCAbZULPZGE-p2EazI2eFBCDktdHtDffJNo7i7ZYSkWkqN9ysr2QZRvYG_PYCzcYSo34Gf5WNvHKuz0Ye3kFkckfuirCmzr3knw2azrSOmpTOX_RSlMlse7HgFYwxHPMJnzPS19ymiwKZPgrAMvCmAUZmsxZGDoKeusNEDGSSFhLcTQys20qGBGYasIgKYGjAKGjK7SCxSOCGBQSU496XBkXQEeOB7k9Sh8jdB0pQGAZw9Ntwvrts2DjIUcsQBv-XEGfnHQXoBmDgzwzYEWxeHd0oNbPIlz7CqvNseoKu6uPZl85xynum6aWd6BDDAtwobbqYkuMUfOUhXf_cH13kWSnuJ6QrOxah94JzAnda3tWRDQ3RajOOjk-OXhbOqi8QMJRFdA_C-xMwQalM_rTSTKOqyCcaNSTkVmMlmyOt90tptk7jKUizDmGhGbsSU8WMY5mhdZ3eUd5O6gQitiMHI1EqnlaRNsXnKFoJ5yHV82Wp1dhFONCG_dlpqunVJD5bFgpxtdFDD-KmXQTymAalFjxeVl_xdc5xd4XYCYmk5dhEiQBE0J_S3Z6x0tmFORpWG9lESK_OBRSul9oKZh9Vet-UZ8FSOVtNFwbeokRwWpFuFL1dL3UpJeININ2cgUfDNWQlwItkokiFf_Kdy12y2O_hqJtoTpNttNxTOiclDzKM1KHNOjYJgTgydcid3mmJl3eA6ezyrDAw1RLCHBucIvYRfwbkmpYMvnfAaA2DIiaTNaSxX8BUl92V49UVKWlQSp8ijfmmTRHrBMmxKjvBIgHqC6dSMhVUEOMzCKXAO3giCS3eZzdrNQGhhqTxpYYnFf6uLoKOIiaGY-ByI1YoIVXxX8aCTOOpesFvHjwOKBEoj4Hoxd3iFMUJQazR7P2drnfmS11kgipM7pSUgB7POKwxEF0NQCedM41wVIuoathAqD6N6qalwQ6iOKlZOBUwwMVAMRDJ3aomG37ZeLYhv6fB0-pUUJSN1q4knjtkLFIJSUrih9FZ0XnOll_aeEgOICqQkb4aOMrovjcJEWvgdjUqGPdyIGgkurfqBRHih3dukUcYxt6Y__4KLQ7acqMx0FOFv0ZxFRTCIRGj_GAlFWUi6fpuPKebXUnEn1PRE0iNXwUV_4jESWb0" - }, - ... - ] - } */ - - const vless_encryption = { - methods: [ - ['mlkem768x25519plus', _('mlkem768x25519plus')] - ], - xormodes: [ - ['native', 'native', _('Native appearance')], - ['xorpub', 'xorpub', _('Eliminate encryption header characteristics')], - ['random', 'random', _('Randomized traffic characteristics')] - ], - tickets: [ - ['600s', '600s', _('Send random ticket of 300s-600s duration for client 0-RTT reuse.')], - ['300-600s', '300-600s', _('Send random ticket of 300s-600s duration for client 0-RTT reuse.')], - ['0s', '0s', _('1-RTT only.')] - ], - rtts: [ - ['0rtt', _('0-RTT reuse.') +' '+ _('Requires server support.')], - ['1rtt', _('1-RTT only.')] - ], - paddings: [ - ['100-111-1111', '100-111-1111: ' + _('After the 1-RTT client/server hello, padding randomly 111-1111 bytes with 100% probability.')], - ['75-0-111', '75-0-111: ' + _('Wait a random 0-111 milliseconds with 75% probability.')], - ['50-0-3333', '50-0-3333: ' + _('Send padding randomly 0-3333 bytes with 50% probability.')] - ], - keypairs: { - types: [ - ['vless-x25519', _('vless-x25519')], - ['vless-mlkem768', _('vless-mlkem768')] - ] - } - }; - - const CBIVlessEncryptionValue = form.Value.extend({ - __name__: 'CBI.VlessEncryptionValue', - - renderWidget: function(section_id, option_index, cfgvalue) { - let node = form.Value.prototype.renderWidget.apply(this, arguments); - - node.classList.add('control-group'); - node.firstChild.style.width = '30em'; - - (node.querySelector('.control-group') || node).appendChild(E('button', { - class: 'cbi-button cbi-button-add', - click: ui.createHandlerFn(this, async (section_id) => { - try { - await navigator.clipboard.writeText(this.formvalue(section_id)); - console.log('Content copied to clipboard!'); - } catch (e) { - console.error('Failed to copy: ', e); - } - /* Deprecated - let inputEl = document.getElementById(this.cbid(section_id)).querySelector('input'); - inputEl.select(); - document.execCommand("copy"); - inputEl.blur(); - */ - return alert(_('Content copied to clipboard!')); - }, section_id) - }, [ _('Copy') ])); - - return node; - }, - - write: function() {} - }); - - class VlessEncryption { - // https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/factory.go#L64 - // https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/factory.go#L12 - constructor(payload) { - this.input = payload || ''; - try { - let content = JSON.parse(this.input.trim()); - Object.keys(content).forEach(key => this[key] = content[key]); - } catch {} - - this.method ||= vless_encryption.methods[0][0]; - this.xormode ||= vless_encryption.xormodes[0][0]; - this.ticket ||= vless_encryption.tickets[0][0]; - this.rtt ||= vless_encryption.rtts[0][0]; - this.paddings ||= []; - this.keypairs ||= []; - } - - setKey(key, value) { - this[key] = value; - - return this - } - - _toMihomo(payload, side) { - if (!['server', 'client'].includes(side)) - throw new Error('Unknown side: ' + side); // `Unknown side: '${side}'` - - let required = [ - payload.method, - payload.xormode, - side === 'server' ? payload.ticket : side === 'client' ? payload.rtt : null - ].join('.'); - - return required + - (isEmpty(payload.paddings) ? '' : '.' + payload.paddings.join('.')) + // Optional - (isEmpty(payload.keypairs) ? '' : '.' + payload.keypairs.map(e => e[side]).join('.')); // Required - } - - toString(format, side) { - format ||= 'json'; - - let payload = removeBlankAttrs({ - method: this.method, - xormode: this.xormode, - ticket: this.ticket, - rtt: this.rtt, - paddings: this.paddings || [], - keypairs: this.keypairs || [] - }); - - if (format === 'json') - return JSON.stringify(payload); - else if (format === 'mihomo') - return this._toMihomo(payload, side); - else - throw new Error(`Unknown format: '${format}'`); - } - } - - let initRequired = function(o, key, uciconfig) { - o.load = function(section_id) { - return new VlessEncryption(uci.get(uciconfig, section_id, 'vless_encryption_hmpayload'))[key]; - } - o.onchange = function(ev, section_id, value) { - let UIEl = this.section.getUIElement(section_id, 'vless_encryption_hmpayload'); - let newentry = new VlessEncryption(UIEl.getValue()).setKey(key, value); - - UIEl.setValue(newentry.toString()); - - [ - ['server', '_vless_encryption_decryption'], - ['client', '_vless_encryption_encryption'] - ].forEach(([side, option]) => { - UIEl = this.section.getUIElement(section_id, option); - UIEl.setValue(newentry.toString('mihomo', side)); - }); - } - o.write = function() {}; - o.rmempty = false; - o.modalonly = true; - } - - let o; - - o = s.taboption('field_vless_encryption', form.Value, 'vless_encryption_hmpayload', _('Payload')); - o.readonly = true; - o.depends('vless_decryption', '1'); - //o.depends('vless_encryption', '1'); - o.modalonly = true; - - o = s.taboption('field_vless_encryption', CBIVlessEncryptionValue, '_vless_encryption_decryption', _('decryption')); - o.readonly = true; - o.depends('vless_decryption', '1'); - o.modalonly = true; - - o = s.taboption('field_vless_encryption', CBIVlessEncryptionValue, '_vless_encryption_encryption', _('encryption')); - o.readonly = true; - o.depends('vless_decryption', '1'); - //o.depends('vless_encryption', '1'); - o.modalonly = true; - - o = s.taboption('field_vless_encryption', form.ListValue, 'vless_encryption_method', _('Encryption method')); - o.default = vless_encryption.methods[0][0]; - vless_encryption.methods.forEach((res) => { - o.value.apply(o, res); - }) - o.depends('vless_decryption', '1'); - //o.depends('vless_encryption', '1'); - initRequired(o, 'method', uciconfig); - - o = s.taboption('field_vless_encryption', form.RichListValue, 'vless_encryption_xormode', _('XOR mode')); - o.default = vless_encryption.xormodes[0][0]; - vless_encryption.xormodes.forEach((res) => { - o.value.apply(o, res); - }) - o.depends('vless_decryption', '1'); - //o.depends('vless_encryption', '1'); - initRequired(o, 'xormode', uciconfig); - - o = s.taboption('field_vless_encryption', CBIRichValue, 'vless_encryption_ticket', _('Server') +' '+ _('RTT')); - o.default = vless_encryption.tickets[0][0]; - vless_encryption.tickets.forEach((res) => { - o.value.apply(o, res); - }) - o.validate = function(section_id, value) { - if (!value) - return true; - - if (!value.match(/^(\d+-)?\d+s$/)) - return _('Expecting: %s').format('^(\\d+-)?\\d+s$'); - - return true; - } - o.depends('vless_decryption', '1'); - initRequired(o, 'ticket', uciconfig); - - o = s.taboption('field_vless_encryption', form.ListValue, 'vless_encryption_rtt', _('Client') +' '+ _('RTT')); - o.default = vless_encryption.rtts[0][0]; - vless_encryption.rtts.forEach((res) => { - o.value.apply(o, res); - }) - o.depends('vless_decryption', '1'); - //o.depends('vless_encryption', '1'); - initRequired(o, 'rtt', uciconfig); - - o = s.taboption('field_vless_encryption', !pr7558_merged ? CBIDynamicList : form.DynamicList, 'vless_encryption_paddings', _('Paddings'), // @pr7558_merged - _('The server and client can set different padding parameters.') + '
' + - _('In the order of one Padding-Length and one Padding-Interval, infinite concatenation.') + '
' + - _('The first padding must have a probability of 100% and at least 35 bytes.')); - vless_encryption.paddings.forEach((res) => { - o.value.apply(o, res); - }) - o.validate = function(section_id, value) { - if (!value) - return true; - - if (!value.match(/^\d+(-\d+){2}$/)) - return _('Expecting: %s').format('^\\d+(-\\d+){2}$'); - - return true; - } - o.allowduplicates = true; - o.depends('vless_decryption', '1'); - //o.depends('vless_encryption', '1'); - initRequired(o, 'paddings', uciconfig); - o.rmempty = true; // Forced - - o = s.taboption('field_vless_encryption', CBIGenText, 'vless_encryption_keypairs', _('Keypairs')); - o.placeholder = '[\n {\n "type": "vless-x25519",\n "server": "cP5Oy9MOpTaBKKE17Pfd56mbb1CIfp5EMpyBYqr2EG8",\n "client": "khEcQMT8j41xWmGYKpZtQ4vd8_9VWyFVmmCDIhRJ-Uk"\n },\n {\n "type": "vless-mlkem768",\n "server": "UHPx3nf-FVxF95byAw0YG025aQNw9HxKej-MiG5AhTcdW_WFpHlTVYQU5NHmXP6tmljSnB2iPmSQ29fisGxEog",\n "client": "h4sdZgCc5-ZefvQ8mZmReOWQdxYb0mwngMdl7pKhYEZZpGWHUPKAmxug87Bgj3GqSHs195QeVpxfrMLNB5J..."\n },\n ...\n]'; - o.rows = 10; - o.hm_options = { - type: vless_encryption.keypairs.types[0][0], - params: '', - callback: function(result) { - const section_id = this.section.section; - const key_type = this.hm_options.type; - - let keypair = {"type": key_type, "server": "", "client": ""}; - switch (key_type) { - case 'vless-x25519': - keypair.server = result.private_key; - keypair.client = result.password; - break; - case 'vless-mlkem768': - keypair.server = result.seed; - keypair.client = result.client; - break; - default: - break; - }; - - let value = []; - try { - value = JSON.parse(this.formvalue(section_id).trim()); - } catch {} - if (!Array.isArray(value)) - value = []; - - value.push(keypair); - - return [ - [this.option, JSON.stringify(value, null, 2)] - ] - } - } - o.renderWidget = function(section_id, option_index, cfgvalue) { - let node = CBITextValue.prototype.renderWidget.apply(this, arguments); - const cbid = this.cbid(section_id) + '._keytype_select'; - const selected = this.hm_options.type; - - let selectEl = E('select', { - id: cbid, - class: 'cbi-input-select', - style: 'width: 10em', - }); - - vless_encryption.keypairs.types.forEach(([k, v]) => { - selectEl.appendChild(E('option', { - 'value': k, - 'selected': (k === selected) ? '' : null - }, [ v ])); - }); - - node.appendChild(E('div', { 'class': 'control-group' }, [ - selectEl, - E('button', { - class: 'cbi-button cbi-button-add', - click: ui.createHandlerFn(this, () => { - this.hm_options.type = document.getElementById(cbid).value; - - return handleGenKey.call(this, this.hm_options); - }) - }, [ _('Generate') ]) - ])); - - return node; - } - o.depends('vless_decryption', '1'); - //o.depends('vless_encryption', '1'); - initRequired(o, 'keypairs', uciconfig); - o.load = function(section_id) { - return JSON.stringify(new VlessEncryption(uci.get(uciconfig, section_id, 'vless_encryption_hmpayload')).keypairs, null, 2); - } - o.onchange = null; // Forced - o.validate = function(section_id, value) { - let result = validateJson.apply(this, arguments); - - if (result === true) { - let arr = JSON.parse(value.trim()); - if (Array.isArray(arr) && arr.length >= 1) { - let UIEl = this.section.getUIElement(section_id, 'vless_encryption_hmpayload'); - let newentry = new VlessEncryption(UIEl.getValue()).setKey('keypairs', JSON.parse(value.trim())); - - UIEl.setValue(newentry.toString()); - - [ - ['server', '_vless_encryption_decryption'], - ['client', '_vless_encryption_encryption'] - ].forEach(([side, option]) => { - UIEl = this.section.getUIElement(section_id, option); - UIEl.setValue(newentry.toString('mihomo', side)); - }); - } else - return _('Expecting: %s').format(_('least one keypair required')); - return true; - } else - return result; - } -} - function handleGenKey(option) { const section_id = this.section.section; const type = this.section.getOption('type')?.formvalue(section_id); @@ -1839,6 +1532,7 @@ return baseclass.extend({ trojan_cipher_methods, tls_client_auth_types, tls_client_fingerprints, + vless_encryption, vless_flow, /* Prototype */ @@ -1857,6 +1551,9 @@ return baseclass.extend({ bool2str, calcStringMD5, decodeBase64Str, + encodeBase64Str, + decodeBase64Bin, + encodeBase64Bin, generateRand, getValue, json2yaml, @@ -1877,7 +1574,6 @@ return baseclass.extend({ updateStatus, getDashURL, renderResDownload, - renderVlessEncryption, handleGenKey, handleReload, handleRemoveIdles, diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js index aea15a00b9..223141c1c6 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js @@ -58,6 +58,54 @@ function parseProviderYaml(field, name, cfg) { return config; } +class VlessEncryptionClient { + // origin: + // https://github.com/XTLS/Xray-core/pull/5067 + // client: + // https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/factory.go#L12 + // https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/client.go#L45 + + constructor(payload) { + this.input = payload || ''; + let content = String.prototype.split.call(this.input, '.'); + + if (content.length >= 4) { + this.method = content[0]; + this.xormode = content[1]; + this.rtt = content[2]; + this.paddings = []; + this.keypairs = []; + + // https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/factory.go#L39 + content.slice(3).forEach((e) => { + if (e.length < 20) + this.paddings.push(e); + else + this.keypairs.push(e); + }); + } else + console.error('Invalid VLESS encryption value: ' + payload); + } + + setKey(key, value) { + this[key] = value; + + return this + } + + toString() { + let required = [ + this.method, + this.xormode, + this.rtt + ].join('.'); + + return required + + (hm.isEmpty(this.paddings) ? '' : '.' + this.paddings.join('.')) + // Optional + (hm.isEmpty(this.keypairs) ? '' : '.' + this.keypairs.join('.')); // Required + } +} + return view.extend({ load() { return Promise.all([ @@ -558,10 +606,72 @@ return view.extend({ so.modalonly = true; /* Vless Encryption fields */ - so = ss.taboption('field_general', form.Value, 'vless_encryption', _('encryption')); + so = ss.taboption('field_general', form.Flag, 'vless_encryption', _('encryption')); + so.default = so.disabled; so.depends('type', 'vless'); so.modalonly = true; + const initVlessEncryptionClientOption = function(o, key) { + o.load = function(section_id) { + const value = uci.get(data[0], section_id, 'vless_encryption_encryption'); + + if (!value) + return null; + + return new VlessEncryptionClient(value)[key]; + } + o.onchange = function(ev, section_id, value) { + let UIEl = this.section.getUIElement(section_id, 'vless_encryption_encryption'); + let newpayload = new VlessEncryptionClient(UIEl.getValue()).setKey(key, value); + + UIEl.setValue(newpayload.toString()); + } + o.write = function() {}; + } + + so = ss.taboption('field_vless_encryption', form.Value, 'vless_encryption_encryption', _('encryption')); + so.renderWidget = function(section_id, option_index, cfgvalue) { + let node = form.Value.prototype.renderWidget.apply(this, arguments); + + node.firstChild.style.width = '30em'; + + return node; + }, + so.rmempty = false; + so.depends('vless_encryption', '1'); + so.modalonly = true; + + so = ss.taboption('field_vless_encryption', form.ListValue, 'vless_encryption_rtt', _('Client') +' '+ _('RTT')); + so.default = hm.vless_encryption.rtts[0][0]; + hm.vless_encryption.rtts.forEach((res) => { + so.value.apply(so, res); + }) + initVlessEncryptionClientOption(so, 'rtt'); + so.rmempty = false; + so.depends('vless_encryption', '1'); + so.modalonly = true; + + so = ss.taboption('field_vless_encryption', !hm.pr7558_merged ? hm.DynamicList : form.DynamicList, 'vless_encryption_paddings', _('Paddings'), // @pr7558_merged + _('The server and client can set different padding parameters.') + '
' + + _('In the order of one Padding-Length and one Padding-Interval, infinite concatenation.') + '
' + + _('The first padding must have a probability of 100% and at least 35 bytes.')); + hm.vless_encryption.paddings.forEach((res) => { + so.value.apply(so, res); + }) + initVlessEncryptionClientOption(so, 'paddings'); + so.validate = function(section_id, value) { + if (!value) + return true; + + if (!value.match(/^\d+(-\d+){2}$/)) + return _('Expecting: %s').format('^\\d+(-\\d+){2}$'); + + return true; + } + so.allowduplicates = true; + so.depends('vless_encryption', '1'); + so.modalonly = true; + /* TLS fields */ so = ss.taboption('field_general', form.Flag, 'tls', _('TLS')); so.default = so.disabled; diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js index 318d2b7719..98aefaf899 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js @@ -7,6 +7,135 @@ 'require fchomo as hm'; +const CBIDummyCopyValue = form.Value.extend({ + __name__: 'CBI.DummyCopyValue', + + readonly: true, + + renderWidget: function(section_id, option_index, cfgvalue) { + let node = form.Value.prototype.renderWidget.apply(this, arguments); + + node.classList.add('control-group'); + node.firstChild.style.width = '30em'; + + node.appendChild(E('button', { + class: 'cbi-button cbi-button-add', + click: ui.createHandlerFn(this, async (section_id) => { + try { + await navigator.clipboard.writeText(this.formvalue(section_id)); + console.log('Content copied to clipboard!'); + } catch (e) { + console.error('Failed to copy: ', e); + } + /* Deprecated + let inputEl = document.getElementById(this.cbid(section_id)).querySelector('input'); + inputEl.select(); + document.execCommand("copy"); + inputEl.blur(); + */ + return alert(_('Content copied to clipboard!')); + }, section_id) + }, [ _('Copy') ])); + + return node; + }, + + write: function() {} +}); + +class VlessEncryption { + // origin: + // https://github.com/XTLS/Xray-core/pull/5067 + // server: + // https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/factory.go#L64 + // https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/server.go#L42 + // client: + // https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/factory.go#L12 + // https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/client.go#L45 +/* +{ + "method": "mlkem768x25519plus", + "xormode": "native", + "ticket": "600s", + "rtt": "0rtt", + "paddings": [ // Optional + "100-111-1111", + "75-0-111", + "50-0-3333", + ... + ], + "keypairs": [ + { + "type": "vless-x25519", + "server": "cP5Oy9MOpTaBKKE17Pfd56mbb1CIfp5EMpyBYqr2EG8", + "client": "khEcQMT8j41xWmGYKpZtQ4vd8_9VWyFVmmCDIhRJ-Uk" + }, + { + "type": "vless-mlkem768", + "server": "UHPx3nf-FVxF95byAw0YG025aQNw9HxKej-MiG5AhTcdW_WFpHlTVYQU5NHmXP6tmljSnB2iPmSQ29fisGxEog", + "client": "h4sdZgCc5-ZefvQ8mZmReOWQdxYb0mwngMdl7pKhYEZZpGWHUPKAmxug87Bgj3GqSHs195QeVpxfrMLNB5Jm0Ge71Fc-A3aLpaS3C3pARbGQoquUDUEVDNwEWjQTvFpGTUV3Nddw_LlRmWN6Wqhguti9cpS6GhEmkBvBayFeHgZosuaQ1FMoAqIeQzSSSoguCZtGLUmdQjEs3zc5rwG1rNanbhtyI3QnooYvr3A0vggIkbmddjtjwYaVQdMAj9Moavc12EAUajOV91QA73RWVuhelbe7pLumsHiW-emIdBgVhEgDDYdGaLq1E8QjB0WbIfufnJp-CJa3Ieu9gmDASTlQBeEREeA9gfoZcTpYD8elhJIJxaPJKXchvUVkFhZarcivlKoqVuaFPzsJM7KQCBC8zfS0t_oiBka-uzg3_Hl153nMTDaCAbZULPZGE-p2EazI2eFBCDktdHtDffJNo7i7ZYSkWkqN9ysr2QZRvYG_PYCzcYSo34Gf5WNvHKuz0Ye3kFkckfuirCmzr3knw2azrSOmpTOX_RSlMlse7HgFYwxHPMJnzPS19ymiwKZPgrAMvCmAUZmsxZGDoKeusNEDGSSFhLcTQys20qGBGYasIgKYGjAKGjK7SCxSOCGBQSU496XBkXQEeOB7k9Sh8jdB0pQGAZw9Ntwvrts2DjIUcsQBv-XEGfnHQXoBmDgzwzYEWxeHd0oNbPIlz7CqvNseoKu6uPZl85xynum6aWd6BDDAtwobbqYkuMUfOUhXf_cH13kWSnuJ6QrOxah94JzAnda3tWRDQ3RajOOjk-OXhbOqi8QMJRFdA_C-xMwQalM_rTSTKOqyCcaNSTkVmMlmyOt90tptk7jKUizDmGhGbsSU8WMY5mhdZ3eUd5O6gQitiMHI1EqnlaRNsXnKFoJ5yHV82Wp1dhFONCG_dlpqunVJD5bFgpxtdFDD-KmXQTymAalFjxeVl_xdc5xd4XYCYmk5dhEiQBE0J_S3Z6x0tmFORpWG9lESK_OBRSul9oKZh9Vet-UZ8FSOVtNFwbeokRwWpFuFL1dL3UpJeININ2cgUfDNWQlwItkokiFf_Kdy12y2O_hqJtoTpNttNxTOiclDzKM1KHNOjYJgTgydcid3mmJl3eA6ezyrDAw1RLCHBucIvYRfwbkmpYMvnfAaA2DIiaTNaSxX8BUl92V49UVKWlQSp8ijfmmTRHrBMmxKjvBIgHqC6dSMhVUEOMzCKXAO3giCS3eZzdrNQGhhqTxpYYnFf6uLoKOIiaGY-ByI1YoIVXxX8aCTOOpesFvHjwOKBEoj4Hoxd3iFMUJQazR7P2drnfmS11kgipM7pSUgB7POKwxEF0NQCedM41wVIuoathAqD6N6qalwQ6iOKlZOBUwwMVAMRDJ3aomG37ZeLYhv6fB0-pUUJSN1q4knjtkLFIJSUrih9FZ0XnOll_aeEgOICqQkb4aOMrovjcJEWvgdjUqGPdyIGgkurfqBRHih3dukUcYxt6Y__4KLQ7acqMx0FOFv0ZxFRTCIRGj_GAlFWUi6fpuPKebXUnEn1PRE0iNXwUV_4jESWb0" + }, + ... + ] +} +*/ + constructor(payload) { + this.input = payload || ''; + try { + let content = JSON.parse(this.input.trim()); + Object.keys(content).forEach(key => this[key] = content[key]); + } catch {} + + this.method ||= hm.vless_encryption.methods[0][0]; + this.xormode ||= hm.vless_encryption.xormodes[0][0]; + this.ticket ||= hm.vless_encryption.tickets[0][0]; + this.rtt ||= hm.vless_encryption.rtts[0][0]; + this.paddings ||= []; + this.keypairs ||= []; + } + + setKey(key, value) { + this[key] = value; + + return this + } + + _toMihomo(payload, side) { + if (!['server', 'client'].includes(side)) + throw new Error('Unknown side: ' + side); // `Unknown side: '${side}'` + + let required = [ + payload.method, + payload.xormode, + side === 'server' ? payload.ticket : side === 'client' ? payload.rtt : null + ].join('.'); + + return required + + (hm.isEmpty(payload.paddings) ? '' : '.' + payload.paddings.join('.')) + // Optional + (hm.isEmpty(payload.keypairs) ? '' : '.' + payload.keypairs.map(e => e[side]).join('.')); // Required + } + + toString(format, side) { + format ||= 'json'; + + let payload = hm.removeBlankAttrs({ + method: this.method, + xormode: this.xormode, + ticket: this.ticket, + rtt: this.rtt, + paddings: this.paddings || [], + keypairs: this.keypairs || [] + }); + + if (format === 'json') + return JSON.stringify(payload); + else if (format === 'mihomo') + return this._toMihomo(payload, side); + else + throw new Error(`Unknown format: '${format}'`); + } +} + return view.extend({ load() { return Promise.all([ @@ -303,7 +432,210 @@ return view.extend({ o.depends('type', 'vless'); o.modalonly = true; - hm.renderVlessEncryption(s, data[0]); + const initVlessEncryptionOption = function(o, key) { + o.load = function(section_id) { + return new VlessEncryption(uci.get(data[0], section_id, 'vless_encryption_hmpayload'))[key]; + } + o.onchange = function(ev, section_id, value) { + let UIEl = this.section.getUIElement(section_id, 'vless_encryption_hmpayload'); + let newpayload = new VlessEncryption(UIEl.getValue()).setKey(key, value); + + UIEl.setValue(newpayload.toString()); + + [ + ['server', '_vless_encryption_decryption'], + ['client', '_vless_encryption_encryption'] + ].forEach(([side, option]) => { + UIEl = this.section.getUIElement(section_id, option); + UIEl.setValue(newpayload.toString('mihomo', side)); + }); + } + o.write = function() {}; + } + + o = s.taboption('field_vless_encryption', form.Value, 'vless_encryption_hmpayload', _('Payload')); + o.readonly = true; + o.depends('vless_decryption', '1'); + o.modalonly = true; + + o = s.taboption('field_vless_encryption', CBIDummyCopyValue, '_vless_encryption_decryption', _('decryption')); + o.depends('vless_decryption', '1'); + o.modalonly = true; + + o = s.taboption('field_vless_encryption', CBIDummyCopyValue, '_vless_encryption_encryption', _('encryption')); + o.depends('vless_decryption', '1'); + o.modalonly = true; + + o = s.taboption('field_vless_encryption', form.ListValue, 'vless_encryption_method', _('Encryption method')); + o.default = hm.vless_encryption.methods[0][0]; + hm.vless_encryption.methods.forEach((res) => { + o.value.apply(o, res); + }) + initVlessEncryptionOption(o, 'method'); + o.depends('vless_decryption', '1'); + o.modalonly = true; + + o = s.taboption('field_vless_encryption', form.RichListValue, 'vless_encryption_xormode', _('XOR mode')); + o.default = hm.vless_encryption.xormodes[0][0]; + hm.vless_encryption.xormodes.forEach((res) => { + o.value.apply(o, res); + }) + initVlessEncryptionOption(o, 'xormode'); + o.depends('vless_decryption', '1'); + o.modalonly = true; + + o = s.taboption('field_vless_encryption', hm.RichValue, 'vless_encryption_ticket', _('Server') +' '+ _('RTT')); + o.default = hm.vless_encryption.tickets[0][0]; + hm.vless_encryption.tickets.forEach((res) => { + o.value.apply(o, res); + }) + initVlessEncryptionOption(o, 'ticket'); + o.validate = function(section_id, value) { + if (!value) + return true; + + if (!value.match(/^(\d+-)?\d+s$/)) + return _('Expecting: %s').format('^(\\d+-)?\\d+s$'); + + return true; + } + o.rmempty = false; + o.depends('vless_decryption', '1'); + o.modalonly = true; + + o = s.taboption('field_vless_encryption', form.ListValue, 'vless_encryption_rtt', _('Client') +' '+ _('RTT')); + o.default = hm.vless_encryption.rtts[0][0]; + hm.vless_encryption.rtts.forEach((res) => { + o.value.apply(o, res); + }) + initVlessEncryptionOption(o, 'rtt'); + o.rmempty = false; + o.depends('vless_decryption', '1'); + o.modalonly = true; + + o = s.taboption('field_vless_encryption', !hm.pr7558_merged ? hm.DynamicList : form.DynamicList, 'vless_encryption_paddings', _('Paddings'), // @pr7558_merged + _('The server and client can set different padding parameters.') + '
' + + _('In the order of one Padding-Length and one Padding-Interval, infinite concatenation.') + '
' + + _('The first padding must have a probability of 100% and at least 35 bytes.')); + hm.vless_encryption.paddings.forEach((res) => { + o.value.apply(o, res); + }) + initVlessEncryptionOption(o, 'paddings'); + o.validate = function(section_id, value) { + if (!value) + return true; + + if (!value.match(/^\d+(-\d+){2}$/)) + return _('Expecting: %s').format('^\\d+(-\\d+){2}$'); + + return true; + } + o.allowduplicates = true; + o.depends('vless_decryption', '1'); + o.modalonly = true; + + o = s.taboption('field_vless_encryption', hm.GenText, 'vless_encryption_keypairs', _('Keypairs')); + o.placeholder = '[\n {\n "type": "vless-x25519",\n "server": "cP5Oy9MOpTaBKKE17Pfd56mbb1CIfp5EMpyBYqr2EG8",\n "client": "khEcQMT8j41xWmGYKpZtQ4vd8_9VWyFVmmCDIhRJ-Uk"\n },\n {\n "type": "vless-mlkem768",\n "server": "UHPx3nf-FVxF95byAw0YG025aQNw9HxKej-MiG5AhTcdW_WFpHlTVYQU5NHmXP6tmljSnB2iPmSQ29fisGxEog",\n "client": "h4sdZgCc5-ZefvQ8mZmReOWQdxYb0mwngMdl7pKhYEZZpGWHUPKAmxug87Bgj3GqSHs195QeVpxfrMLNB5J..."\n },\n ...\n]'; + o.rows = 10; + o.hm_options = { + type: hm.vless_encryption.keypairs.types[0][0], + params: '', + callback: function(result) { + const section_id = this.section.section; + const key_type = this.hm_options.type; + + let keypair = {"type": key_type, "server": "", "client": ""}; + switch (key_type) { + case 'vless-x25519': + keypair.server = result.private_key; + keypair.client = result.password; + break; + case 'vless-mlkem768': + keypair.server = result.seed; + keypair.client = result.client; + break; + default: + break; + } + + let keypairs = []; + try { + keypairs = JSON.parse(this.formvalue(section_id).trim()); + } catch {} + if (!Array.isArray(keypairs)) + keypairs = []; + + keypairs.push(keypair); + + return [ + [this.option, JSON.stringify(keypairs, null, 2)] + ] + } + } + o.renderWidget = function(section_id, option_index, cfgvalue) { + let node = hm.TextValue.prototype.renderWidget.apply(this, arguments); + const cbid = this.cbid(section_id) + '._keytype_select'; + const selected = this.hm_options.type; + + let selectEl = E('select', { + id: cbid, + class: 'cbi-input-select', + style: 'width: 10em', + }); + + hm.vless_encryption.keypairs.types.forEach(([k, v]) => { + selectEl.appendChild(E('option', { + 'value': k, + 'selected': (k === selected) ? '' : null + }, [ v ])); + }); + + node.appendChild(E('div', { 'class': 'control-group' }, [ + selectEl, + E('button', { + class: 'cbi-button cbi-button-add', + click: ui.createHandlerFn(this, () => { + this.hm_options.type = document.getElementById(cbid).value; + + return hm.handleGenKey.call(this, this.hm_options); + }) + }, [ _('Generate') ]) + ])); + + return node; + } + o.load = function(section_id) { + return JSON.stringify(new VlessEncryption(uci.get(data[0], section_id, 'vless_encryption_hmpayload'))['keypairs'], null, 2); + } + o.validate = function(section_id, value) { + let result = hm.validateJson.apply(this, arguments); + + if (result === true) { + let keypairs = JSON.parse(value.trim()); + + if (Array.isArray(keypairs) && keypairs.length >= 1) { + let UIEl = this.section.getUIElement(section_id, 'vless_encryption_hmpayload'); + let newpayload = new VlessEncryption(UIEl.getValue()).setKey('keypairs', keypairs); + + UIEl.setValue(newpayload.toString()); + + [ + ['server', '_vless_encryption_decryption'], + ['client', '_vless_encryption_encryption'] + ].forEach(([side, option]) => { + UIEl = this.section.getUIElement(section_id, option); + UIEl.setValue(newpayload.toString('mihomo', side)); + }); + } else + return _('Expecting: %s').format(_('least one keypair required')); + + return true; + } else + return result; + } + o.rmempty = false; + o.depends('vless_decryption', '1'); + o.modalonly = true; /* TLS fields */ o = s.taboption('field_general', form.Flag, 'tls', _('TLS')); diff --git a/small/luci-app-fchomo/root/etc/uci-defaults/99_luci-app-fchomo-migration b/small/luci-app-fchomo/root/etc/uci-defaults/99_luci-app-fchomo-migration index e4c094f933..e28d56ec69 100755 --- a/small/luci-app-fchomo/root/etc/uci-defaults/99_luci-app-fchomo-migration +++ b/small/luci-app-fchomo/root/etc/uci-defaults/99_luci-app-fchomo-migration @@ -4,9 +4,6 @@ sed -i 's|^FQDN:$|DOMAIN:|' "/etc/fchomo/resources/direct_list.yaml" 2>/dev/null sed -i 's|^FQDN:$|DOMAIN:|' "/etc/fchomo/resources/proxy_list.yaml" 2>/dev/null -# mieru_port_range -> mieru_ports -sed -i 's|^\toption mieru_port_range |\tlist mieru_ports |' /etc/config/fchomo 2>/dev/null - # default_proxy -> client_enabled and MATCH rule default_proxy=$(uci -q get fchomo.routing.default_proxy) if [ -n "$default_proxy" ]; then diff --git a/small/luci-app-fchomo/root/etc/uci-defaults/99_luci-app-fchomo-migration_node b/small/luci-app-fchomo/root/etc/uci-defaults/99_luci-app-fchomo-migration_node new file mode 100755 index 0000000000..691f6c9f5d --- /dev/null +++ b/small/luci-app-fchomo/root/etc/uci-defaults/99_luci-app-fchomo-migration_node @@ -0,0 +1,43 @@ +#!/bin/sh +# Migration script for fchomo node +# Used to migrate LuCI application nodes option. + +. /lib/functions.sh +. /usr/share/libubox/jshn.sh + +CONF=fchomo + +config_load "$CONF" + +# isDefined