diff --git a/.github/update.log b/.github/update.log index 0295fa401f..c28799a111 100644 --- a/.github/update.log +++ b/.github/update.log @@ -872,3 +872,4 @@ Update On Tue Dec 31 19:31:50 CET 2024 Update On Wed Jan 1 19:33:17 CET 2025 Update On Thu Jan 2 19:33:11 CET 2025 Update On Fri Jan 3 19:34:50 CET 2025 +Update On Sat Jan 4 19:31:12 CET 2025 diff --git a/clash-nyanpasu/backend/Cargo.lock b/clash-nyanpasu/backend/Cargo.lock index 79032395c4..44a1bd968b 100644 --- a/clash-nyanpasu/backend/Cargo.lock +++ b/clash-nyanpasu/backend/Cargo.lock @@ -1586,7 +1586,7 @@ dependencies = [ "tracing-subscriber", "url", "uuid", - "webview2-com 0.33.0", + "webview2-com", "which 7.0.1", "whoami", "window-vibrancy", @@ -8436,7 +8436,7 @@ dependencies = [ "url", "urlpattern", "webkit2gtk", - "webview2-com 0.34.0", + "webview2-com", "window-vibrancy", "windows 0.58.0", ] @@ -8745,7 +8745,7 @@ dependencies = [ "tauri-utils", "url", "webkit2gtk", - "webview2-com 0.34.0", + "webview2-com", "windows 0.58.0", "wry", ] @@ -10225,20 +10225,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "webview2-com" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61ff3d9d0ee4efcb461b14eb3acfda2702d10dc329f339303fc3e57215ae2c" -dependencies = [ - "webview2-com-macros", - "webview2-com-sys 0.33.0", - "windows 0.58.0", - "windows-core 0.58.0", - "windows-implement 0.58.0", - "windows-interface 0.58.0", -] - [[package]] name = "webview2-com" version = "0.34.0" @@ -10246,7 +10232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "823e7ebcfaea51e78f72c87fc3b65a1e602c321f407a0b36dbb327d7bb7cd921" dependencies = [ "webview2-com-macros", - "webview2-com-sys 0.34.0", + "webview2-com-sys", "windows 0.58.0", "windows-core 0.58.0", "windows-implement 0.58.0", @@ -10264,17 +10250,6 @@ dependencies = [ "syn 2.0.94", ] -[[package]] -name = "webview2-com-sys" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3a3e2eeb58f82361c93f9777014668eb3d07e7d174ee4c819575a9208011886" -dependencies = [ - "thiserror 1.0.69", - "windows 0.58.0", - "windows-core 0.58.0", -] - [[package]] name = "webview2-com-sys" version = "0.34.0" @@ -11021,7 +10996,7 @@ dependencies = [ "url", "webkit2gtk", "webkit2gtk-sys", - "webview2-com 0.34.0", + "webview2-com", "windows 0.58.0", "windows-core 0.58.0", "windows-version", diff --git a/clash-nyanpasu/backend/tauri/Cargo.toml b/clash-nyanpasu/backend/tauri/Cargo.toml index 0d56bc54c6..c9875d26a1 100644 --- a/clash-nyanpasu/backend/tauri/Cargo.toml +++ b/clash-nyanpasu/backend/tauri/Cargo.toml @@ -214,7 +214,7 @@ windows-sys = { version = "0.59", features = [ "Win32_System_SystemInformation", ] } windows-core = "0.58.0" -webview2-com = "0.33" +webview2-com = "0.34" [features] default = ["custom-protocol", "default-meta"] diff --git a/clash-nyanpasu/frontend/nyanpasu/package.json b/clash-nyanpasu/frontend/nyanpasu/package.json index 8479052ac5..1580c7c02f 100644 --- a/clash-nyanpasu/frontend/nyanpasu/package.json +++ b/clash-nyanpasu/frontend/nyanpasu/package.json @@ -16,9 +16,9 @@ "@emotion/styled": "11.14.0", "@juggle/resize-observer": "3.4.0", "@material/material-color-utilities": "0.3.0", - "@mui/icons-material": "6.3.0", - "@mui/lab": "6.0.0-beta.21", - "@mui/material": "6.3.0", + "@mui/icons-material": "6.3.1", + "@mui/lab": "6.0.0-beta.22", + "@mui/material": "6.3.1", "@nyanpasu/interface": "workspace:^", "@nyanpasu/ui": "workspace:^", "@tanstack/router-zod-adapter": "1.81.5", @@ -54,7 +54,7 @@ "@emotion/react": "11.14.0", "@iconify/json": "2.2.291", "@monaco-editor/react": "4.6.0", - "@tanstack/react-query": "5.62.11", + "@tanstack/react-query": "5.62.15", "@tanstack/react-router": "1.89.2", "@tanstack/router-devtools": "1.89.2", "@tanstack/router-plugin": "1.87.13", @@ -79,7 +79,7 @@ "meta-json-schema": "1.19.1", "monaco-yaml": "5.2.3", "nanoid": "5.0.9", - "sass-embedded": "1.83.0", + "sass-embedded": "1.83.1", "shiki": "1.26.1", "tailwindcss-textshadow": "2.1.3", "unplugin-auto-import": "0.19.0", diff --git a/clash-nyanpasu/frontend/ui/package.json b/clash-nyanpasu/frontend/ui/package.json index 5b61d0a3af..82b96ed4f7 100644 --- a/clash-nyanpasu/frontend/ui/package.json +++ b/clash-nyanpasu/frontend/ui/package.json @@ -17,9 +17,9 @@ }, "dependencies": { "@material/material-color-utilities": "0.3.0", - "@mui/icons-material": "6.3.0", - "@mui/lab": "6.0.0-beta.21", - "@mui/material": "6.3.0", + "@mui/icons-material": "6.3.1", + "@mui/lab": "6.0.0-beta.22", + "@mui/material": "6.3.1", "@radix-ui/react-portal": "1.1.3", "@radix-ui/react-scroll-area": "1.2.2", "@tauri-apps/api": "2.2.0", @@ -42,7 +42,7 @@ "@types/d3-interpolate-path": "2.0.3", "clsx": "2.1.1", "d3-interpolate-path": "2.3.0", - "sass-embedded": "1.83.0", + "sass-embedded": "1.83.1", "tailwind-merge": "2.6.0", "typescript-plugin-css-modules": "5.1.0", "vite-plugin-dts": "4.4.0" diff --git a/clash-nyanpasu/package.json b/clash-nyanpasu/package.json index 26eb7a0334..8130752515 100644 --- a/clash-nyanpasu/package.json +++ b/clash-nyanpasu/package.json @@ -61,7 +61,7 @@ "@commitlint/config-conventional": "19.6.0", "@eslint/compat": "1.2.4", "@ianvs/prettier-plugin-sort-imports": "4.4.0", - "@tauri-apps/cli": "2.2.0", + "@tauri-apps/cli": "2.2.2", "@types/fs-extra": "11.0.4", "@types/lodash-es": "4.17.12", "@types/node": "22.10.5", diff --git a/clash-nyanpasu/pnpm-lock.yaml b/clash-nyanpasu/pnpm-lock.yaml index 2b65671bc9..537f203b8d 100644 --- a/clash-nyanpasu/pnpm-lock.yaml +++ b/clash-nyanpasu/pnpm-lock.yaml @@ -31,8 +31,8 @@ importers: specifier: 4.4.0 version: 4.4.0(prettier@3.4.2) '@tauri-apps/cli': - specifier: 2.2.0 - version: 2.2.0 + specifier: 2.2.2 + version: 2.2.2 '@types/fs-extra': specifier: 11.0.4 version: 11.0.4 @@ -212,14 +212,14 @@ importers: specifier: 0.3.0 version: 0.3.0 '@mui/icons-material': - specifier: 6.3.0 - version: 6.3.0(@mui/material@6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(react@19.0.0) + specifier: 6.3.1 + version: 6.3.1(@mui/material@6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(react@19.0.0) '@mui/lab': - specifier: 6.0.0-beta.21 - version: 6.0.0-beta.21(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@mui/material@6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: 6.0.0-beta.22 + version: 6.0.0-beta.22(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@mui/material@6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mui/material': - specifier: 6.3.0 - version: 6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: 6.3.1 + version: 6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@nyanpasu/interface': specifier: workspace:^ version: link:../interface @@ -261,13 +261,13 @@ importers: version: 0.4.0 material-react-table: specifier: 3.1.0 - version: 3.1.0(2dir25pziwwbjwxjpqxtarsyyy) + version: 3.1.0(hierfampdha7urqfyzohwumpoq) monaco-editor: specifier: 0.52.2 version: 0.52.2 mui-color-input: specifier: 5.0.1 - version: 5.0.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@mui/material@6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 5.0.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@mui/material@6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: specifier: 19.0.0 version: 19.0.0 @@ -282,7 +282,7 @@ importers: version: 1.6.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react-hook-form-mui: specifier: 7.4.1 - version: 7.4.1(ezstqsbb2r4zi3rernt3lablby) + version: 7.4.1(jlu5wpapoynq5o37r6urceyqa4) react-i18next: specifier: 15.4.0 version: 15.4.0(i18next@24.2.0(typescript@5.7.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -321,8 +321,8 @@ importers: specifier: 4.6.0 version: 4.6.0(monaco-editor@0.52.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@tanstack/react-query': - specifier: 5.62.11 - version: 5.62.11(react@19.0.0) + specifier: 5.62.15 + version: 5.62.15(react@19.0.0) '@tanstack/react-router': specifier: 1.89.2 version: 1.89.2(@tanstack/router-generator@1.87.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -331,7 +331,7 @@ importers: version: 1.89.2(@tanstack/react-router@1.89.2(@tanstack/router-generator@1.87.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(csstype@3.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@tanstack/router-plugin': specifier: 1.87.13 - version: 1.87.13(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) + version: 1.87.13(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) '@tauri-apps/plugin-clipboard-manager': specifier: 2.2.0 version: 2.2.0 @@ -367,13 +367,13 @@ importers: version: 13.12.2 '@vitejs/plugin-legacy': specifier: 6.0.0 - version: 6.0.0(terser@5.36.0)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) + version: 6.0.0(terser@5.36.0)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) '@vitejs/plugin-react': specifier: 4.3.4 - version: 4.3.4(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) + version: 4.3.4(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) '@vitejs/plugin-react-swc': specifier: 3.7.2 - version: 3.7.2(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) + version: 3.7.2(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) change-case: specifier: 5.4.4 version: 5.4.4 @@ -396,8 +396,8 @@ importers: specifier: 5.0.9 version: 5.0.9 sass-embedded: - specifier: 1.83.0 - version: 1.83.0 + specifier: 1.83.1 + version: 1.83.1 shiki: specifier: 1.26.1 version: 1.26.1 @@ -415,19 +415,19 @@ importers: version: 13.12.0 vite: specifier: 6.0.7 - version: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) + version: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) vite-plugin-html: specifier: 3.2.2 - version: 3.2.2(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) + version: 3.2.2(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) vite-plugin-sass-dts: specifier: 1.3.30 - version: 1.3.30(postcss@8.4.49)(prettier@3.4.2)(sass-embedded@1.83.0)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) + version: 1.3.30(postcss@8.4.49)(prettier@3.4.2)(sass-embedded@1.83.1)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) vite-plugin-svgr: specifier: 4.3.0 - version: 4.3.0(rollup@4.27.4)(typescript@5.7.2)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) + version: 4.3.0(rollup@4.27.4)(typescript@5.7.2)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) vite-tsconfig-paths: specifier: 5.1.4 - version: 5.1.4(typescript@5.7.2)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) + version: 5.1.4(typescript@5.7.2)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) zod: specifier: 3.24.1 version: 3.24.1 @@ -438,14 +438,14 @@ importers: specifier: 0.3.0 version: 0.3.0 '@mui/icons-material': - specifier: 6.3.0 - version: 6.3.0(@mui/material@6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(react@19.0.0) + specifier: 6.3.1 + version: 6.3.1(@mui/material@6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(react@19.0.0) '@mui/lab': - specifier: 6.0.0-beta.21 - version: 6.0.0-beta.21(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@mui/material@6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: 6.0.0-beta.22 + version: 6.0.0-beta.22(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@mui/material@6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mui/material': - specifier: 6.3.0 - version: 6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: 6.3.1 + version: 6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-portal': specifier: 1.1.3 version: 1.1.3(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -463,7 +463,7 @@ importers: version: 19.0.2 '@vitejs/plugin-react': specifier: 4.3.4 - version: 4.3.4(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) + version: 4.3.4(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) ahooks: specifier: 3.8.4 version: 3.8.4(react@19.0.0) @@ -490,10 +490,10 @@ importers: version: 17.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) vite: specifier: 6.0.7 - version: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) + version: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) vite-tsconfig-paths: specifier: 5.1.4 - version: 5.1.4(typescript@5.7.2)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) + version: 5.1.4(typescript@5.7.2)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) devDependencies: '@emotion/react': specifier: 11.14.0 @@ -508,8 +508,8 @@ importers: specifier: 2.3.0 version: 2.3.0 sass-embedded: - specifier: 1.83.0 - version: 1.83.0 + specifier: 1.83.1 + version: 1.83.1 tailwind-merge: specifier: 2.6.0 version: 2.6.0 @@ -518,7 +518,7 @@ importers: version: 5.1.0(typescript@5.7.2) vite-plugin-dts: specifier: 4.4.0 - version: 4.4.0(@types/node@22.10.5)(rollup@4.27.4)(typescript@5.7.2)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) + version: 4.4.0(@types/node@22.10.5)(rollup@4.27.4)(typescript@5.7.2)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)) scripts: dependencies: @@ -1831,28 +1831,28 @@ packages: '@types/react': optional: true - '@mui/core-downloads-tracker@6.3.0': - resolution: {integrity: sha512-/d8NwSuC3rMwCjswmGB3oXC4sdDuhIUJ8inVQAxGrADJhf0eq/kmy+foFKvpYhHl2siOZR+MLdFttw6/Bzqtqg==} + '@mui/core-downloads-tracker@6.3.1': + resolution: {integrity: sha512-2OmnEyoHpj5//dJJpMuxOeLItCCHdf99pjMFfUFdBteCunAK9jW+PwEo4mtdGcLs7P+IgZ+85ypd52eY4AigoQ==} - '@mui/icons-material@6.3.0': - resolution: {integrity: sha512-3uWws6DveDn5KxCS34p+sUNMxehuclQY6OmoJeJJ+Sfg9L7LGBpksY/nX5ywKAqickTZnn+sQyVcp963ep9jvw==} + '@mui/icons-material@6.3.1': + resolution: {integrity: sha512-nJmWj1PBlwS3t1PnoqcixIsftE+7xrW3Su7f0yrjPw4tVjYrgkhU0hrRp+OlURfZ3ptdSkoBkalee9Bhf1Erfw==} engines: {node: '>=14.0.0'} peerDependencies: - '@mui/material': ^6.3.0 + '@mui/material': ^6.3.1 '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: '@types/react': optional: true - '@mui/lab@6.0.0-beta.21': - resolution: {integrity: sha512-hiFZgTwBNhJMUlEhmqfW4+5wy3C8UF9KFuzSOux6x4kgc9hsC0l+motXcF1Vyh+jhJYGeZ6yUoImqCf9RWzEvw==} + '@mui/lab@6.0.0-beta.22': + resolution: {integrity: sha512-9nwUfBj+UzoQJOCbqV+JcCSJ74T+gGWrM1FMlXzkahtYUcMN+5Zmh2ArlttW3zv2dZyCzp7K5askcnKF0WzFQg==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.5.0 '@emotion/styled': ^11.3.0 - '@mui/material': ^6.3.0 - '@mui/material-pigment-css': ^6.3.0 + '@mui/material': ^6.3.1 + '@mui/material-pigment-css': ^6.3.1 '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1866,13 +1866,13 @@ packages: '@types/react': optional: true - '@mui/material@6.3.0': - resolution: {integrity: sha512-qhlTFyRMxfoVPxUtA5e8IvqxP0dWo2Ij7cvot7Orag+etUlZH+3UwD8gZGt+3irOoy7Ms3UNBflYjwEikUXtAQ==} + '@mui/material@6.3.1': + resolution: {integrity: sha512-ynG9ayhxgCsHJ/dtDcT1v78/r2GwQyP3E0hPz3GdPRl0uFJz/uUTtI5KFYwadXmbC+Uv3bfB8laZ6+Cpzh03gA==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.5.0 '@emotion/styled': ^11.3.0 - '@mui/material-pigment-css': ^6.3.0 + '@mui/material-pigment-css': ^6.3.1 '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1896,8 +1896,8 @@ packages: '@types/react': optional: true - '@mui/private-theming@6.3.0': - resolution: {integrity: sha512-tdS8jvqMokltNTXg6ioRCCbVdDmZUJZa/T9VtTnX2Lwww3FTgCakst9tWLZSxm1fEE9Xp0m7onZJmgeUmWQYVw==} + '@mui/private-theming@6.3.1': + resolution: {integrity: sha512-g0u7hIUkmXmmrmmf5gdDYv9zdAig0KoxhIQn1JN8IVqApzf/AyRhH3uDGx5mSvs8+a1zb4+0W6LC260SyTTtdQ==} engines: {node: '>=14.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1919,8 +1919,8 @@ packages: '@emotion/styled': optional: true - '@mui/styled-engine@6.3.0': - resolution: {integrity: sha512-iWA6eyiPkO07AlHxRUvI7dwVRSc+84zV54kLmjUms67GJeOWVuXlu8ZO+UhCnwJxHacghxnabsMEqet5PYQmHg==} + '@mui/styled-engine@6.3.1': + resolution: {integrity: sha512-/7CC0d2fIeiUxN5kCCwYu4AWUDd9cCTxWCyo0v/Rnv6s8uk6hWgJC3VLZBoDENBHf/KjqDZuYJ2CR+7hD6QYww==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.4.1 @@ -1948,8 +1948,8 @@ packages: '@types/react': optional: true - '@mui/system@6.3.0': - resolution: {integrity: sha512-L+8hUHMNlfReKSqcnVslFrVhoNfz/jw7Fe9NfDE85R3KarvZ4O3MU9daF/lZeqEAvnYxEilkkTfDwQ7qCgJdFg==} + '@mui/system@6.3.1': + resolution: {integrity: sha512-AwqQ3EAIT2np85ki+N15fF0lFXX1iFPqenCzVOSl3QXKy2eifZeGd9dGtt7pGMoFw5dzW4dRGGzRpLAq9rkl7A==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -1964,8 +1964,8 @@ packages: '@types/react': optional: true - '@mui/types@7.2.20': - resolution: {integrity: sha512-straFHD7L8v05l/N5vcWk+y7eL9JF0C2mtph/y4BPm3gn2Eh61dDwDB65pa8DLss3WJfDXYC7Kx5yjP0EmXpgw==} + '@mui/types@7.2.21': + resolution: {integrity: sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: @@ -1982,8 +1982,8 @@ packages: '@types/react': optional: true - '@mui/utils@6.3.0': - resolution: {integrity: sha512-MkDBF08OPVwXhAjedyMykRojgvmf0y/jxkBWjystpfI/pasyTYrfdv4jic6s7j3y2+a+SJzS9qrD6X8ZYj/8AQ==} + '@mui/utils@6.3.1': + resolution: {integrity: sha512-sjGjXAngoio6lniQZKJ5zGfjm+LD2wvLwco7FbKe1fu8A7VIFmz2SwkLb+MDPLNX1lE7IscvNNyh1pobtZg2tw==} engines: {node: '>=14.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -2720,11 +2720,11 @@ packages: resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} engines: {node: '>=12'} - '@tanstack/query-core@5.62.9': - resolution: {integrity: sha512-lwePd8hNYhyQ4nM/iRQ+Wz2cDtspGeZZHFZmCzHJ7mfKXt+9S301fULiY2IR2byJYY6Z03T427E5PoVfMexHjw==} + '@tanstack/query-core@5.62.15': + resolution: {integrity: sha512-wT20X14CxcWY8YLJ/1pnsXn/y1Q2uRJZYWW93PWRtZt+3/JlGZyiyTcO4pGnqycnP7CokCROAyatsraosqZsDA==} - '@tanstack/react-query@5.62.11': - resolution: {integrity: sha512-Xb1nw0cYMdtFmwkvH9+y5yYFhXvLRCnXoqlzSw7UkqtCVFq3cG8q+rHZ2Yz1XrC+/ysUaTqbLKJqk95mCgC1oQ==} + '@tanstack/react-query@5.62.15': + resolution: {integrity: sha512-Ny3xxsOWmEQCFyHiV3CF7t6+QAV+LpBEREiXyllKR4+tStyd8smOAa98ZHmEx0ZNy36M31K8enifB5wTSYAKJw==} peerDependencies: react: ^18 || ^19 @@ -2815,68 +2815,68 @@ packages: '@tauri-apps/api@2.2.0': resolution: {integrity: sha512-R8epOeZl1eJEl603aUMIGb4RXlhPjpgxbGVEaqY+0G5JG9vzV/clNlzTeqc+NLYXVqXcn8mb4c5b9pJIUDEyAg==} - '@tauri-apps/cli-darwin-arm64@2.2.0': - resolution: {integrity: sha512-Paxq9JUj1uxV/CDr8hNnr88GcjXLmmX1yBcclQldeUoAoSgsXN1eLLS4e0CHXIzYATmcsSbcl6Zgquo4wW4e9w==} + '@tauri-apps/cli-darwin-arm64@2.2.2': + resolution: {integrity: sha512-JMXbX5hGrLOzJbjisd7gBe25PmHouXA1+f4yVWg5PRlgxW7pla7krOzhu2mchFlMVDr8aLwhMLgohvvx+raXag==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tauri-apps/cli-darwin-x64@2.2.0': - resolution: {integrity: sha512-BpIQxYQmg+vyS4Z4hxnAcrR95Kn2cnQbkA37NZDW0x7nnx+IFTNremU0cxATJQ3vk1PPVi3PQgrnAX5HE5aXKA==} + '@tauri-apps/cli-darwin-x64@2.2.2': + resolution: {integrity: sha512-i2gxKXev+Ed0UWeq0xSiyRjSkzzBbu9MCOhs/QVv1YuV+097K/fF89f+a4v5JJftCq2IOHhSKH23KyTJBLnNKQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tauri-apps/cli-linux-arm-gnueabihf@2.2.0': - resolution: {integrity: sha512-JDYwiELRKKOStSA2QKpGiOZL4si+9ZZcwYCh7DRxnk0csM42vIv0bgcG+ucK5A/gltyDyavZgHNToxFGXaLKXA==} + '@tauri-apps/cli-linux-arm-gnueabihf@2.2.2': + resolution: {integrity: sha512-iC2HndsN5smmbvEDUQFyTHyYHSgx7OwJ6puyXLLpkAHnQDo4TGSPxIlPeZFSZmEoaJEmHLdG3j1LcFWOKrxWQg==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tauri-apps/cli-linux-arm64-gnu@2.2.0': - resolution: {integrity: sha512-ITpT44YDMrg3l0ZQMTeFjj+mRzjCZAGyD3GuJ414aL5X1GWO2CHMdAHYQT9gYtH2OHlixaWq9nRJXDL7W9998w==} + '@tauri-apps/cli-linux-arm64-gnu@2.2.2': + resolution: {integrity: sha512-YzK30tleUzWxfIp1davc5RhvmNZxiZQkUnQ4zajGJZ99zxNk8kwvv8nYSC3/J2R8sYpnuv+7CzNyIwA2s6yH+w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-arm64-musl@2.2.0': - resolution: {integrity: sha512-sj/NR2O99XeatAh4UgM4MltNipoi4A8ZbMhTb1/FaGEvs/ZVrushIX69hFIyxcHu8Nbe/OkZb5b+N80A/V30jA==} + '@tauri-apps/cli-linux-arm64-musl@2.2.2': + resolution: {integrity: sha512-nOw2apBzOCTiH1fLOjL42ajHNhMzdp640CX5RrWkDYdyVO7YbGmWzrN26PPXohScScXVjSjtDDxdeQV1gHCxhg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-x64-gnu@2.2.0': - resolution: {integrity: sha512-mwX7EzQLKbHZ/VzSksiOLS0uqJUVDqY58cqCj2puiGaPVgCE7bxbKcO6P3PmqYX9P8XWR2K8t922k7K44dScqA==} + '@tauri-apps/cli-linux-x64-gnu@2.2.2': + resolution: {integrity: sha512-Tmm4qVY8yxSugi8sCko1dyZxyPGK8m3tWm+b1J0DXwzxqaoMqNXuYGxkwtUdkznPXEfQSD8OGBfwjXNmVGE91Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-linux-x64-musl@2.2.0': - resolution: {integrity: sha512-9C8lVjVJPEjMZaOQdKA5SZV1NpjLP690Y9GodRMum5cIDE3T1AVrp9w6vjrsciQN42agTsO8UaREppF6OjxYnA==} + '@tauri-apps/cli-linux-x64-musl@2.2.2': + resolution: {integrity: sha512-AwAcaCUhmwzOFPvje80g2BAhnkoEpXdl1E0Uk+lvr9makHM0+aV++M5jibD97yxKnK5NrQ9YXPH5Sn6CdncgUg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-win32-arm64-msvc@2.2.0': - resolution: {integrity: sha512-lbXYsLlvyanx1S/VSiylc6fMsDh1wnBpBid4VxscSfSPyyIOPC/p9uJvcEFWR5Caaet6vUwyJ3iKUaT5yqfsUA==} + '@tauri-apps/cli-win32-arm64-msvc@2.2.2': + resolution: {integrity: sha512-u7TnwuUAN+eX6R2kDfSM8fsUFiBzNqq9PnAOsQ2qbwbHGbu7mHfGO1OFgnIzBt1C9FolFbENk2pzjiL4R9baXQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tauri-apps/cli-win32-ia32-msvc@2.2.0': - resolution: {integrity: sha512-nlQ2iRFJTuZw5YelO2HNIsrkcZdqlt7HxdMkHJtCQkFReclpSupjbn6CrupIXHYO8NO6LcoDXiteOYhdWh0fJg==} + '@tauri-apps/cli-win32-ia32-msvc@2.2.2': + resolution: {integrity: sha512-9KScbGKU6GfHThEYWdlO0+COW/8SDfIXbYgEvEcfZztE4VedHBbI0XfU+l+aS8nJN+fvYX+DtvY0tpDwyo0G4A==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@tauri-apps/cli-win32-x64-msvc@2.2.0': - resolution: {integrity: sha512-MupuZwaJqJv3YsNCGD0H/98S+7106E6HChGoobuPmN87eCniSL/mSsOaa0cVmxdIAwVcSi0fuDzDBRMt8wOhHw==} + '@tauri-apps/cli-win32-x64-msvc@2.2.2': + resolution: {integrity: sha512-ko8OWCLwFaHfdBaKbRX/C5btNt61v17qKOSQPksuc5PVvY0tAoci09612nEMlYiogZKEtn7VAqSdRAG6h0tz+g==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tauri-apps/cli@2.2.0': - resolution: {integrity: sha512-8sA4LBU4kA73SZcL/mLjXvrpJjDqLFLQymg39FDlf3SVWyzbQxCyHBrokCLhUXLenG4qWVCl91oAImC/s47vzA==} + '@tauri-apps/cli@2.2.2': + resolution: {integrity: sha512-5fVEdP4t4BT0ymvXZAM78kB0S/TaiRDLDoSRWGxVy1e7XCwuKyST5m6ybeyw/h/soK/91tbf+W3xXXy7XzkT4A==} engines: {node: '>= 10'} hasBin: true @@ -6971,128 +6971,128 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sass-embedded-android-arm64@1.83.0: - resolution: {integrity: sha512-GBiCvM4a2rkWBLdYDxI6XYnprfk5U5c81g69RC2X6kqPuzxzx8qTArQ9M6keFK4+iDQ5N9QTwFCr0KbZTn+ZNQ==} + sass-embedded-android-arm64@1.83.1: + resolution: {integrity: sha512-S63rlLPGCA9FCqYYOobDJrwcuBX0zbSOl7y0jT9DlfqeqNOkC6NIT1id6RpMFCs3uhd4gbBS2E/5WPv5J5qwbw==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [android] - sass-embedded-android-arm@1.83.0: - resolution: {integrity: sha512-uwFSXzJlfbd4Px189xE5l+cxN8+TQpXdQgJec7TIrb4HEY7imabtpYufpVdqUVwT1/uiis5V4+qIEC4Vl5XObQ==} + sass-embedded-android-arm@1.83.1: + resolution: {integrity: sha512-FKfrmwDG84L5cfn8fmIew47qnCFFUdcoOTCzOw8ROItkRhLLH0hnIm6gEpG5T6OFf6kxzUxvE9D0FvYQUznZrw==} engines: {node: '>=14.0.0'} cpu: [arm] os: [android] - sass-embedded-android-ia32@1.83.0: - resolution: {integrity: sha512-5ATPdGo2SICqAhiJl/Z8KQ23zH4sGgobGgux0TnrNtt83uHZ+r+To/ubVJ7xTkZxed+KJZnIpolGD8dQyQqoTg==} + sass-embedded-android-ia32@1.83.1: + resolution: {integrity: sha512-AGlY2vFLJhF2hN0qOz12f4eDs6x0b5BUapOpgfRrqQLHIfJhxkvi39bInsiBgQ57U0jb4I7AaS2e2e+sj7+Rqw==} engines: {node: '>=14.0.0'} cpu: [ia32] os: [android] - sass-embedded-android-riscv64@1.83.0: - resolution: {integrity: sha512-aveknUOB8GZewOzVn2Uwk+DKcncTR50Q6vtzslNMGbYnxtgQNHzy8A1qVEviNUruex+pHofppeMK4iMPFAbiEQ==} + sass-embedded-android-riscv64@1.83.1: + resolution: {integrity: sha512-OyU4AnfAUVd/wBaT60XvHidmQdaEsVUnxvI71oyPM/id1v97aWTZX3SmGkwGb7uA/q6Soo2uNalgvOSNJn7PwA==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [android] - sass-embedded-android-x64@1.83.0: - resolution: {integrity: sha512-WqIay/72ncyf9Ph4vS742J3a73wZihWmzFUwpn1OD6lme1Aj4eWzWIve5IVnlTEJgcZcDHu6ECID9IZgehJKoA==} + sass-embedded-android-x64@1.83.1: + resolution: {integrity: sha512-NY5rwffhF4TnhXVErZnfFIjHqU3MNoWxCuSHumRN3dDI8hp8+IF59W5+Qw9AARlTXvyb+D0u5653aLSea5F40w==} engines: {node: '>=14.0.0'} cpu: [x64] os: [android] - sass-embedded-darwin-arm64@1.83.0: - resolution: {integrity: sha512-XQl9QqgxFFIPm/CzHhmppse5o9ocxrbaAdC2/DAnlAqvYWBBtgFqPjGoYlej13h9SzfvNoogx+y9r+Ap+e+hYg==} + sass-embedded-darwin-arm64@1.83.1: + resolution: {integrity: sha512-w1SBcSkIgIWgUfB7IKcPoTbSwnS3Kag5PVv3e3xfW6ZCsDweYZLQntUd2WGgaoekdm1uIbVuvPxnDH2t880iGQ==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [darwin] - sass-embedded-darwin-x64@1.83.0: - resolution: {integrity: sha512-ERQ7Tvp1kFOW3ux4VDFIxb7tkYXHYc+zJpcrbs0hzcIO5ilIRU2tIOK1OrNwrFO6Qxyf7AUuBwYKLAtIU/Nz7g==} + sass-embedded-darwin-x64@1.83.1: + resolution: {integrity: sha512-RWrmLtUhEP5kvcGOAFdr99/ebZ/eW9z3FAktLldvgl2k96WSTC1Zr2ctL0E+Y+H3uLahEZsshIFk6RkVIRKIsA==} engines: {node: '>=14.0.0'} cpu: [x64] os: [darwin] - sass-embedded-linux-arm64@1.83.0: - resolution: {integrity: sha512-syEAVTJt4qhaMLxrSwOWa46zdqHJdnqJkLUK+t9aCr8xqBZLPxSUeIGji76uOehQZ1C+KGFj6n9xstHN6wzOJw==} + sass-embedded-linux-arm64@1.83.1: + resolution: {integrity: sha512-HVIytzj8OO18fmBY6SVRIYErcJ+Nd9a5RNF6uArav/CqvwPLATlUV8dwqSyWQIzSsQUhDF/vFIlJIoNLKKzD3A==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] - sass-embedded-linux-arm@1.83.0: - resolution: {integrity: sha512-baG9RYBJxUFmqwDNC9h9ZFElgJoyO3jgHGjzEZ1wHhIS9anpG+zZQvO8bHx3dBpKEImX+DBeLX+CxsFR9n81gQ==} + sass-embedded-linux-arm@1.83.1: + resolution: {integrity: sha512-y7rHuRgjg2YM284rin068PsEdthPljSGb653Slut5Wba4A2IP11UNVraSl6Je2AYTuoPRjQX0g7XdsrjXlzC3g==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] - sass-embedded-linux-ia32@1.83.0: - resolution: {integrity: sha512-RRBxQxMpoxu5+XcSSc6QR/o9asEwUzR8AbCS83RaXcdTIHTa/CccQsiAoDDoPlRsMTLqnzs0LKL4CfOsf7zBbA==} + sass-embedded-linux-ia32@1.83.1: + resolution: {integrity: sha512-/pc+jHllyvfaYYLTRCoXseRc4+V3Z7IDPqsviTcfVdICAoR9mgK2RtIuIZanhm1NP/lDylDOgvj1NtjcA2dNvg==} engines: {node: '>=14.0.0'} cpu: [ia32] os: [linux] - sass-embedded-linux-musl-arm64@1.83.0: - resolution: {integrity: sha512-Y7juhPHClUO2H5O+u+StRy6SEAcwZ+hTEk5WJdEmo1Bb1gDtfHvJaWB/iFZJ2tW0W1e865AZeUrC4OcOFjyAQA==} + sass-embedded-linux-musl-arm64@1.83.1: + resolution: {integrity: sha512-wjSIYYqdIQp3DjliSTYNFg04TVqQf/3Up/Stahol0Qf/TTjLkjHHtT2jnDaZI5GclHi2PVJqQF3wEGB8bGJMzQ==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] - sass-embedded-linux-musl-arm@1.83.0: - resolution: {integrity: sha512-Yc7u2TelCfBab+PRob9/MNJFh3EooMiz4urvhejXkihTiKSHGCv5YqDdtWzvyb9tY2Jb7YtYREVuHwfdVn3dTQ==} + sass-embedded-linux-musl-arm@1.83.1: + resolution: {integrity: sha512-sFM8GXOVoeR91j9MiwNRcFXRpTA7u4185SaGuvUjcRMb84mHvtWOJPGDvgZqbWdVClBRJp6J7+CShliWngy/og==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] - sass-embedded-linux-musl-ia32@1.83.0: - resolution: {integrity: sha512-arQeYwGmwXV8byx5G1PtSzZWW1jbkfR5qrIHMEbTFSAvAxpqjgSvCvrHMOFd73FcMxVaYh4BX9LQNbKinkbEdg==} + sass-embedded-linux-musl-ia32@1.83.1: + resolution: {integrity: sha512-iwhTH5gwmoGt3VH6dn4WV8N6eWvthKAvUX5XPURq7e9KEsc7QP8YNHagwaAJh7TAPopb32buyEg6oaUmzxUI+Q==} engines: {node: '>=14.0.0'} cpu: [ia32] os: [linux] - sass-embedded-linux-musl-riscv64@1.83.0: - resolution: {integrity: sha512-E6uzlIWz59rut+Z3XR6mLG915zNzv07ISvj3GUNZENdHM7dF8GQ//ANoIpl5PljMQKp89GnYdvo6kj2gnaBf/g==} + sass-embedded-linux-musl-riscv64@1.83.1: + resolution: {integrity: sha512-FjFNWHU1n0Q6GpK1lAHQL5WmzlPjL8DTVLkYW2A/dq8EsutAdi3GfpeyWZk9bte8kyWdmPUWG3BHlnQl22xdoA==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] - sass-embedded-linux-musl-x64@1.83.0: - resolution: {integrity: sha512-eAMK6tyGqvqr21r9g8BnR3fQc1rYFj85RGduSQ3xkITZ6jOAnOhuU94N5fwRS852Hpws0lXhET+7JHXgg3U18w==} + sass-embedded-linux-musl-x64@1.83.1: + resolution: {integrity: sha512-BUfYR5TIDvgGHWhxSIKwTJocXU88ECZ0BW89RJqtvr7m83fKdf5ylTFCOieU7BwcA7SORUeZzcQzVFIdPUM3BQ==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] - sass-embedded-linux-riscv64@1.83.0: - resolution: {integrity: sha512-Ojpi78pTv02sy2fUYirRGXHLY3fPnV/bvwuC2i5LwPQw2LpCcFyFTtN0c5h4LJDk9P6wr+/ZB/JXU8tHIOlK+Q==} + sass-embedded-linux-riscv64@1.83.1: + resolution: {integrity: sha512-KOBGSpMrJi8y+H+za3vAAVQImPUvQa5eUrvTbbOl+wkU7WAGhOu8xrxgmYYiz3pZVBBcfRjz4I2jBcDFKJmWSw==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] - sass-embedded-linux-x64@1.83.0: - resolution: {integrity: sha512-3iLjlXdoPfgZRtX4odhRvka1BQs5mAXqfCtDIQBgh/o0JnGPzJIWWl9bYLpHxK8qb+uyVBxXYgXpI0sCzArBOw==} + sass-embedded-linux-x64@1.83.1: + resolution: {integrity: sha512-swUsMHKqlEU9dZQ/I5WADDaXz+QkmJS27x/Oeh+oz41YgZ0ppKd0l4Vwjn0LgOQn+rxH1zLFv6xXDycvj68F/w==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] - sass-embedded-win32-arm64@1.83.0: - resolution: {integrity: sha512-iOHw/8/t2dlTW3lOFwG5eUbiwhEyGWawivlKWJ8lkXH7fjMpVx2VO9zCFAm8RvY9xOHJ9sf1L7g5bx3EnNP9BQ==} + sass-embedded-win32-arm64@1.83.1: + resolution: {integrity: sha512-6lONEBN5TaFD5L/y68zUugryXqm4RAFuLdaOPeZQRu+7ay/AmfhtFYfE5gRssnIcIx1nlcoq7zA3UX+SN2jo1Q==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [win32] - sass-embedded-win32-ia32@1.83.0: - resolution: {integrity: sha512-2PxNXJ8Pad4geVcTXY4rkyTr5AwbF8nfrCTDv0ulbTvPhzX2mMKEGcBZUXWn5BeHZTBc6whNMfS7d5fQXR9dDQ==} + sass-embedded-win32-ia32@1.83.1: + resolution: {integrity: sha512-HxZDkAE9n6Gb8Rz6xd67VHuo5FkUSQ4xPb7cHKa4pE0ndwH5Oc0uEhbqjJobpgmnuTm1rQYNU2nof1sFhy2MFA==} engines: {node: '>=14.0.0'} cpu: [ia32] os: [win32] - sass-embedded-win32-x64@1.83.0: - resolution: {integrity: sha512-muBXkFngM6eLTNqOV0FQi7Dv9s+YRQ42Yem26mosdan/GmJQc81deto6uDTgrYn+bzFNmiXcOdfm+0MkTWK3OQ==} + sass-embedded-win32-x64@1.83.1: + resolution: {integrity: sha512-5Q0aPfUaqRek8Ee1AqTUIC0o6yQSA8QwyhCgh7upsnHG3Ltm8pkJOYjzm+UgYPJeoMNppDjdDlRGQISE7qzd4g==} engines: {node: '>=14.0.0'} cpu: [x64] os: [win32] - sass-embedded@1.83.0: - resolution: {integrity: sha512-/8cYZeL39evUqe0o//193na51Q1VWZ61qhxioQvLJwOtWIrX+PgNhCyD8RSuTtmzc4+6+waFZf899bfp/MCUwA==} + sass-embedded@1.83.1: + resolution: {integrity: sha512-LdKG6nxLEzpXbMUt0if12PhUNonGvy91n7IWHOZRZjvA6AWm9oVdhpO+KEXN/Sc+jjGvQeQcav9+Z8DwmII/pA==} engines: {node: '>=16.0.0'} hasBin: true @@ -9619,8 +9619,8 @@ snapshots: dependencies: '@babel/runtime': 7.26.0 '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mui/types': 7.2.20(@types/react@19.0.2) - '@mui/utils': 6.3.0(@types/react@19.0.2)(react@19.0.0) + '@mui/types': 7.2.21(@types/react@19.0.2) + '@mui/utils': 6.3.1(@types/react@19.0.2)(react@19.0.0) '@popperjs/core': 2.11.8 clsx: 2.1.1 prop-types: 15.8.1 @@ -9629,24 +9629,24 @@ snapshots: optionalDependencies: '@types/react': 19.0.2 - '@mui/core-downloads-tracker@6.3.0': {} + '@mui/core-downloads-tracker@6.3.1': {} - '@mui/icons-material@6.3.0(@mui/material@6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(react@19.0.0)': + '@mui/icons-material@6.3.1(@mui/material@6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(react@19.0.0)': dependencies: '@babel/runtime': 7.26.0 - '@mui/material': 6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mui/material': 6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 optionalDependencies: '@types/react': 19.0.2 - '@mui/lab@6.0.0-beta.21(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@mui/material@6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@mui/lab@6.0.0-beta.22(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@mui/material@6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.26.0 '@mui/base': 5.0.0-beta.68(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mui/material': 6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mui/system': 6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0) - '@mui/types': 7.2.20(@types/react@19.0.2) - '@mui/utils': 6.3.0(@types/react@19.0.2)(react@19.0.0) + '@mui/material': 6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mui/system': 6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0) + '@mui/types': 7.2.21(@types/react@19.0.2) + '@mui/utils': 6.3.1(@types/react@19.0.2)(react@19.0.0) clsx: 2.1.1 prop-types: 15.8.1 react: 19.0.0 @@ -9656,13 +9656,13 @@ snapshots: '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0) '@types/react': 19.0.2 - '@mui/material@6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@mui/material@6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.26.0 - '@mui/core-downloads-tracker': 6.3.0 - '@mui/system': 6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0) - '@mui/types': 7.2.20(@types/react@19.0.2) - '@mui/utils': 6.3.0(@types/react@19.0.2)(react@19.0.0) + '@mui/core-downloads-tracker': 6.3.1 + '@mui/system': 6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0) + '@mui/types': 7.2.21(@types/react@19.0.2) + '@mui/utils': 6.3.1(@types/react@19.0.2)(react@19.0.0) '@popperjs/core': 2.11.8 '@types/react-transition-group': 4.4.12(@types/react@19.0.2) clsx: 2.1.1 @@ -9686,10 +9686,10 @@ snapshots: optionalDependencies: '@types/react': 19.0.2 - '@mui/private-theming@6.3.0(@types/react@19.0.2)(react@19.0.0)': + '@mui/private-theming@6.3.1(@types/react@19.0.2)(react@19.0.0)': dependencies: '@babel/runtime': 7.26.0 - '@mui/utils': 6.3.0(@types/react@19.0.2)(react@19.0.0) + '@mui/utils': 6.3.1(@types/react@19.0.2)(react@19.0.0) prop-types: 15.8.1 react: 19.0.0 optionalDependencies: @@ -9706,7 +9706,7 @@ snapshots: '@emotion/react': 11.14.0(@types/react@19.0.2)(react@19.0.0) '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0) - '@mui/styled-engine@6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(react@19.0.0)': + '@mui/styled-engine@6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.26.0 '@emotion/cache': 11.14.0 @@ -9724,7 +9724,7 @@ snapshots: '@babel/runtime': 7.26.0 '@mui/private-theming': 5.16.6(@types/react@19.0.2)(react@19.0.0) '@mui/styled-engine': 5.16.6(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(react@19.0.0) - '@mui/types': 7.2.20(@types/react@19.0.2) + '@mui/types': 7.2.21(@types/react@19.0.2) '@mui/utils': 5.16.6(@types/react@19.0.2)(react@19.0.0) clsx: 2.1.1 csstype: 3.1.3 @@ -9735,13 +9735,13 @@ snapshots: '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0) '@types/react': 19.0.2 - '@mui/system@6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0)': + '@mui/system@6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0)': dependencies: '@babel/runtime': 7.26.0 - '@mui/private-theming': 6.3.0(@types/react@19.0.2)(react@19.0.0) - '@mui/styled-engine': 6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(react@19.0.0) - '@mui/types': 7.2.20(@types/react@19.0.2) - '@mui/utils': 6.3.0(@types/react@19.0.2)(react@19.0.0) + '@mui/private-theming': 6.3.1(@types/react@19.0.2)(react@19.0.0) + '@mui/styled-engine': 6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(react@19.0.0) + '@mui/types': 7.2.21(@types/react@19.0.2) + '@mui/utils': 6.3.1(@types/react@19.0.2)(react@19.0.0) clsx: 2.1.1 csstype: 3.1.3 prop-types: 15.8.1 @@ -9751,14 +9751,14 @@ snapshots: '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0) '@types/react': 19.0.2 - '@mui/types@7.2.20(@types/react@19.0.2)': + '@mui/types@7.2.21(@types/react@19.0.2)': optionalDependencies: '@types/react': 19.0.2 '@mui/utils@5.16.6(@types/react@19.0.2)(react@19.0.0)': dependencies: '@babel/runtime': 7.26.0 - '@mui/types': 7.2.20(@types/react@19.0.2) + '@mui/types': 7.2.21(@types/react@19.0.2) '@types/prop-types': 15.7.14 clsx: 2.1.1 prop-types: 15.8.1 @@ -9767,10 +9767,10 @@ snapshots: optionalDependencies: '@types/react': 19.0.2 - '@mui/utils@6.3.0(@types/react@19.0.2)(react@19.0.0)': + '@mui/utils@6.3.1(@types/react@19.0.2)(react@19.0.0)': dependencies: '@babel/runtime': 7.26.0 - '@mui/types': 7.2.20(@types/react@19.0.2) + '@mui/types': 7.2.21(@types/react@19.0.2) '@types/prop-types': 15.7.14 clsx: 2.1.1 prop-types: 15.8.1 @@ -9779,11 +9779,11 @@ snapshots: optionalDependencies: '@types/react': 19.0.2 - '@mui/x-date-pickers@7.9.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@mui/material@6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@mui/x-date-pickers@7.9.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@mui/material@6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.26.0 '@mui/base': 5.0.0-beta.68(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mui/material': 6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mui/material': 6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mui/system': 5.16.7(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0) '@mui/utils': 5.16.6(@types/react@19.0.2)(react@19.0.0) '@types/react-transition-group': 4.4.12(@types/react@19.0.2) @@ -10461,11 +10461,11 @@ snapshots: dependencies: remove-accents: 0.5.0 - '@tanstack/query-core@5.62.9': {} + '@tanstack/query-core@5.62.15': {} - '@tanstack/react-query@5.62.11(react@19.0.0)': + '@tanstack/react-query@5.62.15(react@19.0.0)': dependencies: - '@tanstack/query-core': 5.62.9 + '@tanstack/query-core': 5.62.15 react: 19.0.0 '@tanstack/react-router@1.89.2(@tanstack/router-generator@1.87.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': @@ -10516,7 +10516,7 @@ snapshots: tsx: 4.19.2 zod: 3.24.1 - '@tanstack/router-plugin@1.87.13(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1))': + '@tanstack/router-plugin@1.87.13(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1))': dependencies: '@babel/core': 7.26.0 '@babel/generator': 7.26.3 @@ -10537,7 +10537,7 @@ snapshots: unplugin: 1.16.0 zod: 3.24.1 optionalDependencies: - vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) transitivePeerDependencies: - supports-color @@ -10562,48 +10562,48 @@ snapshots: '@tauri-apps/api@2.2.0': {} - '@tauri-apps/cli-darwin-arm64@2.2.0': + '@tauri-apps/cli-darwin-arm64@2.2.2': optional: true - '@tauri-apps/cli-darwin-x64@2.2.0': + '@tauri-apps/cli-darwin-x64@2.2.2': optional: true - '@tauri-apps/cli-linux-arm-gnueabihf@2.2.0': + '@tauri-apps/cli-linux-arm-gnueabihf@2.2.2': optional: true - '@tauri-apps/cli-linux-arm64-gnu@2.2.0': + '@tauri-apps/cli-linux-arm64-gnu@2.2.2': optional: true - '@tauri-apps/cli-linux-arm64-musl@2.2.0': + '@tauri-apps/cli-linux-arm64-musl@2.2.2': optional: true - '@tauri-apps/cli-linux-x64-gnu@2.2.0': + '@tauri-apps/cli-linux-x64-gnu@2.2.2': optional: true - '@tauri-apps/cli-linux-x64-musl@2.2.0': + '@tauri-apps/cli-linux-x64-musl@2.2.2': optional: true - '@tauri-apps/cli-win32-arm64-msvc@2.2.0': + '@tauri-apps/cli-win32-arm64-msvc@2.2.2': optional: true - '@tauri-apps/cli-win32-ia32-msvc@2.2.0': + '@tauri-apps/cli-win32-ia32-msvc@2.2.2': optional: true - '@tauri-apps/cli-win32-x64-msvc@2.2.0': + '@tauri-apps/cli-win32-x64-msvc@2.2.2': optional: true - '@tauri-apps/cli@2.2.0': + '@tauri-apps/cli@2.2.2': optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.2.0 - '@tauri-apps/cli-darwin-x64': 2.2.0 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.2.0 - '@tauri-apps/cli-linux-arm64-gnu': 2.2.0 - '@tauri-apps/cli-linux-arm64-musl': 2.2.0 - '@tauri-apps/cli-linux-x64-gnu': 2.2.0 - '@tauri-apps/cli-linux-x64-musl': 2.2.0 - '@tauri-apps/cli-win32-arm64-msvc': 2.2.0 - '@tauri-apps/cli-win32-ia32-msvc': 2.2.0 - '@tauri-apps/cli-win32-x64-msvc': 2.2.0 + '@tauri-apps/cli-darwin-arm64': 2.2.2 + '@tauri-apps/cli-darwin-x64': 2.2.2 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.2.2 + '@tauri-apps/cli-linux-arm64-gnu': 2.2.2 + '@tauri-apps/cli-linux-arm64-musl': 2.2.2 + '@tauri-apps/cli-linux-x64-gnu': 2.2.2 + '@tauri-apps/cli-linux-x64-musl': 2.2.2 + '@tauri-apps/cli-win32-arm64-msvc': 2.2.2 + '@tauri-apps/cli-win32-ia32-msvc': 2.2.2 + '@tauri-apps/cli-win32-x64-msvc': 2.2.2 '@tauri-apps/plugin-clipboard-manager@2.2.0': dependencies: @@ -11027,7 +11027,7 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitejs/plugin-legacy@6.0.0(terser@5.36.0)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1))': + '@vitejs/plugin-legacy@6.0.0(terser@5.36.0)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1))': dependencies: '@babel/core': 7.26.0 '@babel/preset-env': 7.26.0(@babel/core@7.26.0) @@ -11038,25 +11038,25 @@ snapshots: regenerator-runtime: 0.14.1 systemjs: 6.15.1 terser: 5.36.0 - vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react-swc@3.7.2(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1))': + '@vitejs/plugin-react-swc@3.7.2(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1))': dependencies: '@swc/core': 1.7.26 - vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@4.3.4(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1))': + '@vitejs/plugin-react@4.3.4(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1))': dependencies: '@babel/core': 7.26.0 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) transitivePeerDependencies: - supports-color @@ -13947,13 +13947,13 @@ snapshots: escape-string-regexp: 4.0.0 optional: true - material-react-table@3.1.0(2dir25pziwwbjwxjpqxtarsyyy): + material-react-table@3.1.0(hierfampdha7urqfyzohwumpoq): dependencies: '@emotion/react': 11.14.0(@types/react@19.0.2)(react@19.0.0) '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0) - '@mui/icons-material': 6.3.0(@mui/material@6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(react@19.0.0) - '@mui/material': 6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mui/x-date-pickers': 7.9.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@mui/material@6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mui/icons-material': 6.3.1(@mui/material@6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(react@19.0.0) + '@mui/material': 6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mui/x-date-pickers': 7.9.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@mui/material@6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@tanstack/match-sorter-utils': 8.19.4 '@tanstack/react-table': 8.20.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@tanstack/react-virtual': 3.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -14298,12 +14298,12 @@ snapshots: muggle-string@0.4.1: {} - mui-color-input@5.0.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@mui/material@6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + mui-color-input@5.0.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@mui/material@6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@ctrl/tinycolor': 4.1.0 '@emotion/react': 11.14.0(@types/react@19.0.2)(react@19.0.0) '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0) - '@mui/material': 6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mui/material': 6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) optionalDependencies: @@ -14949,14 +14949,14 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - react-hook-form-mui@7.4.1(ezstqsbb2r4zi3rernt3lablby): + react-hook-form-mui@7.4.1(jlu5wpapoynq5o37r6urceyqa4): dependencies: - '@mui/material': 6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mui/material': 6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 react-hook-form: 7.52.1(react@19.0.0) optionalDependencies: - '@mui/icons-material': 6.3.0(@mui/material@6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(react@19.0.0) - '@mui/x-date-pickers': 7.9.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@mui/material@6.3.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mui/icons-material': 6.3.1(@mui/material@6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(react@19.0.0) + '@mui/x-date-pickers': 7.9.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@mui/material@6.3.1(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.2)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react-hook-form@7.52.1(react@19.0.0): dependencies: @@ -15299,67 +15299,67 @@ snapshots: safer-buffer@2.1.2: {} - sass-embedded-android-arm64@1.83.0: + sass-embedded-android-arm64@1.83.1: optional: true - sass-embedded-android-arm@1.83.0: + sass-embedded-android-arm@1.83.1: optional: true - sass-embedded-android-ia32@1.83.0: + sass-embedded-android-ia32@1.83.1: optional: true - sass-embedded-android-riscv64@1.83.0: + sass-embedded-android-riscv64@1.83.1: optional: true - sass-embedded-android-x64@1.83.0: + sass-embedded-android-x64@1.83.1: optional: true - sass-embedded-darwin-arm64@1.83.0: + sass-embedded-darwin-arm64@1.83.1: optional: true - sass-embedded-darwin-x64@1.83.0: + sass-embedded-darwin-x64@1.83.1: optional: true - sass-embedded-linux-arm64@1.83.0: + sass-embedded-linux-arm64@1.83.1: optional: true - sass-embedded-linux-arm@1.83.0: + sass-embedded-linux-arm@1.83.1: optional: true - sass-embedded-linux-ia32@1.83.0: + sass-embedded-linux-ia32@1.83.1: optional: true - sass-embedded-linux-musl-arm64@1.83.0: + sass-embedded-linux-musl-arm64@1.83.1: optional: true - sass-embedded-linux-musl-arm@1.83.0: + sass-embedded-linux-musl-arm@1.83.1: optional: true - sass-embedded-linux-musl-ia32@1.83.0: + sass-embedded-linux-musl-ia32@1.83.1: optional: true - sass-embedded-linux-musl-riscv64@1.83.0: + sass-embedded-linux-musl-riscv64@1.83.1: optional: true - sass-embedded-linux-musl-x64@1.83.0: + sass-embedded-linux-musl-x64@1.83.1: optional: true - sass-embedded-linux-riscv64@1.83.0: + sass-embedded-linux-riscv64@1.83.1: optional: true - sass-embedded-linux-x64@1.83.0: + sass-embedded-linux-x64@1.83.1: optional: true - sass-embedded-win32-arm64@1.83.0: + sass-embedded-win32-arm64@1.83.1: optional: true - sass-embedded-win32-ia32@1.83.0: + sass-embedded-win32-ia32@1.83.1: optional: true - sass-embedded-win32-x64@1.83.0: + sass-embedded-win32-x64@1.83.1: optional: true - sass-embedded@1.83.0: + sass-embedded@1.83.1: dependencies: '@bufbuild/protobuf': 2.2.3 buffer-builder: 0.2.0 @@ -15370,26 +15370,26 @@ snapshots: sync-child-process: 1.0.2 varint: 6.0.0 optionalDependencies: - sass-embedded-android-arm: 1.83.0 - sass-embedded-android-arm64: 1.83.0 - sass-embedded-android-ia32: 1.83.0 - sass-embedded-android-riscv64: 1.83.0 - sass-embedded-android-x64: 1.83.0 - sass-embedded-darwin-arm64: 1.83.0 - sass-embedded-darwin-x64: 1.83.0 - sass-embedded-linux-arm: 1.83.0 - sass-embedded-linux-arm64: 1.83.0 - sass-embedded-linux-ia32: 1.83.0 - sass-embedded-linux-musl-arm: 1.83.0 - sass-embedded-linux-musl-arm64: 1.83.0 - sass-embedded-linux-musl-ia32: 1.83.0 - sass-embedded-linux-musl-riscv64: 1.83.0 - sass-embedded-linux-musl-x64: 1.83.0 - sass-embedded-linux-riscv64: 1.83.0 - sass-embedded-linux-x64: 1.83.0 - sass-embedded-win32-arm64: 1.83.0 - sass-embedded-win32-ia32: 1.83.0 - sass-embedded-win32-x64: 1.83.0 + sass-embedded-android-arm: 1.83.1 + sass-embedded-android-arm64: 1.83.1 + sass-embedded-android-ia32: 1.83.1 + sass-embedded-android-riscv64: 1.83.1 + sass-embedded-android-x64: 1.83.1 + sass-embedded-darwin-arm64: 1.83.1 + sass-embedded-darwin-x64: 1.83.1 + sass-embedded-linux-arm: 1.83.1 + sass-embedded-linux-arm64: 1.83.1 + sass-embedded-linux-ia32: 1.83.1 + sass-embedded-linux-musl-arm: 1.83.1 + sass-embedded-linux-musl-arm64: 1.83.1 + sass-embedded-linux-musl-ia32: 1.83.1 + sass-embedded-linux-musl-riscv64: 1.83.1 + sass-embedded-linux-musl-x64: 1.83.1 + sass-embedded-linux-riscv64: 1.83.1 + sass-embedded-linux-x64: 1.83.1 + sass-embedded-win32-arm64: 1.83.1 + sass-embedded-win32-ia32: 1.83.1 + sass-embedded-win32-x64: 1.83.1 sass@1.83.0: dependencies: @@ -16416,7 +16416,7 @@ snapshots: - rollup - supports-color - vite-plugin-dts@4.4.0(@types/node@22.10.5)(rollup@4.27.4)(typescript@5.7.2)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)): + vite-plugin-dts@4.4.0(@types/node@22.10.5)(rollup@4.27.4)(typescript@5.7.2)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)): dependencies: '@microsoft/api-extractor': 7.48.1(@types/node@22.10.5) '@rollup/pluginutils': 5.1.4(rollup@4.27.4) @@ -16429,13 +16429,13 @@ snapshots: magic-string: 0.30.17 typescript: 5.7.2 optionalDependencies: - vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite-plugin-html@3.2.2(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)): + vite-plugin-html@3.2.2(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)): dependencies: '@rollup/pluginutils': 4.2.1 colorette: 2.0.20 @@ -16449,39 +16449,39 @@ snapshots: html-minifier-terser: 6.1.0 node-html-parser: 5.4.2 pathe: 0.2.0 - vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) - vite-plugin-sass-dts@1.3.30(postcss@8.4.49)(prettier@3.4.2)(sass-embedded@1.83.0)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)): + vite-plugin-sass-dts@1.3.30(postcss@8.4.49)(prettier@3.4.2)(sass-embedded@1.83.1)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)): dependencies: postcss: 8.4.49 postcss-js: 4.0.1(postcss@8.4.49) prettier: 3.4.2 - sass-embedded: 1.83.0 - vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) + sass-embedded: 1.83.1 + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) - vite-plugin-svgr@4.3.0(rollup@4.27.4)(typescript@5.7.2)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)): + vite-plugin-svgr@4.3.0(rollup@4.27.4)(typescript@5.7.2)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)): dependencies: '@rollup/pluginutils': 5.1.3(rollup@4.27.4) '@svgr/core': 8.1.0(typescript@5.7.2) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.7.2)) - vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) transitivePeerDependencies: - rollup - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.7.2)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)): + vite-tsconfig-paths@5.1.4(typescript@5.7.2)(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1)): dependencies: debug: 4.3.7 globrex: 0.1.2 tsconfck: 3.0.3(typescript@5.7.2) optionalDependencies: - vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) + vite: 6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1) transitivePeerDependencies: - supports-color - typescript - vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.0)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1): + vite@6.0.7(@types/node@22.10.5)(jiti@2.4.1)(less@4.2.0)(sass-embedded@1.83.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.6.1): dependencies: esbuild: 0.24.2 postcss: 8.4.49 @@ -16492,7 +16492,7 @@ snapshots: jiti: 2.4.1 less: 4.2.0 sass: 1.83.0 - sass-embedded: 1.83.0 + sass-embedded: 1.83.1 stylus: 0.62.0 terser: 5.36.0 tsx: 4.19.2 diff --git a/hysteria/app/internal/tun/check_ipv6_others.go b/hysteria/app/internal/tun/check_ipv6_others.go new file mode 100644 index 0000000000..7cfd9ab14e --- /dev/null +++ b/hysteria/app/internal/tun/check_ipv6_others.go @@ -0,0 +1,14 @@ +//go:build !unix && !windows + +package tun + +import "net" + +func isIPv6Supported() bool { + lis, err := net.ListenPacket("udp6", "[::1]:0") + if err != nil { + return false + } + _ = lis.Close() + return true +} diff --git a/hysteria/app/internal/tun/check_ipv6_unix.go b/hysteria/app/internal/tun/check_ipv6_unix.go new file mode 100644 index 0000000000..8fdffaf410 --- /dev/null +++ b/hysteria/app/internal/tun/check_ipv6_unix.go @@ -0,0 +1,16 @@ +//go:build unix + +package tun + +import ( + "golang.org/x/sys/unix" +) + +func isIPv6Supported() bool { + sock, err := unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_UDP) + if err != nil { + return false + } + _ = unix.Close(sock) + return true +} diff --git a/hysteria/app/internal/tun/check_ipv6_windows.go b/hysteria/app/internal/tun/check_ipv6_windows.go new file mode 100644 index 0000000000..d488d7e636 --- /dev/null +++ b/hysteria/app/internal/tun/check_ipv6_windows.go @@ -0,0 +1,24 @@ +//go:build windows + +package tun + +import ( + "golang.org/x/sys/windows" +) + +func isIPv6Supported() bool { + var wsaData windows.WSAData + err := windows.WSAStartup(uint32(0x202), &wsaData) + if err != nil { + // Failing silently: it is not our duty to report such errors + return true + } + defer windows.WSACleanup() + + sock, err := windows.Socket(windows.AF_INET6, windows.SOCK_DGRAM, windows.IPPROTO_UDP) + if err != nil { + return false + } + _ = windows.Closesocket(sock) + return true +} diff --git a/hysteria/app/internal/tun/server.go b/hysteria/app/internal/tun/server.go index 303d4ec567..a999051bbe 100644 --- a/hysteria/app/internal/tun/server.go +++ b/hysteria/app/internal/tun/server.go @@ -49,6 +49,10 @@ type EventLogger interface { } func (s *Server) Serve() error { + if !isIPv6Supported() { + s.Logger.Warn("tun-pre-check", zap.String("msg", "IPv6 is not supported or enabled on this system, TUN device is created without IPv6 support.")) + s.Inet6Address = nil + } tunOpts := tun.Options{ Name: s.IfName, Inet4Address: s.Inet4Address, diff --git a/lede/package/boot/uboot-rockchip/patches/302-rockchip-rk3399-Add-support-for-rumu3f-fine3399.patch b/lede/package/boot/uboot-rockchip/patches/302-rockchip-rk3399-Add-support-for-rumu3f-fine3399.patch index 65da8842ae..d4ff19239f 100644 --- a/lede/package/boot/uboot-rockchip/patches/302-rockchip-rk3399-Add-support-for-rumu3f-fine3399.patch +++ b/lede/package/boot/uboot-rockchip/patches/302-rockchip-rk3399-Add-support-for-rumu3f-fine3399.patch @@ -1,4 +1,21 @@ --- /dev/null ++++ b/arch/arm/dts/rk3399-fine-3399-u-boot.dtsi +@@ -0,0 +1,14 @@ ++// SPDX-License-Identifier: GPL-2.0+ ++ ++#include "rk3399-u-boot.dtsi" ++#include "rk3399-sdram-lpddr4-100.dtsi" ++ ++/ { ++ chosen { ++ u-boot,spl-boot-order = "same-as-spl", &sdhci, &sdmmc; ++ }; ++}; ++ ++&vdd_log { ++ regulator-init-microvolt = <950000>; ++}; +--- /dev/null +++ b/arch/arm/dts/rk3399-fine-3399.dts @@ -0,0 +1,789 @@ +// SPDX-License-Identifier: (GPL-2.0+ OR MIT) diff --git a/mieru/deployments/docker/mita-amd64/conf/config.json b/mieru/deployments/docker/mita/conf/config.json similarity index 100% rename from mieru/deployments/docker/mita-amd64/conf/config.json rename to mieru/deployments/docker/mita/conf/config.json diff --git a/mieru/deployments/docker/mita-amd64/docker-compose.yml b/mieru/deployments/docker/mita/docker-compose.yml similarity index 100% rename from mieru/deployments/docker/mita-amd64/docker-compose.yml rename to mieru/deployments/docker/mita/docker-compose.yml diff --git a/mieru/deployments/docker/mita-amd64/dockerfile b/mieru/deployments/docker/mita/dockerfile similarity index 76% rename from mieru/deployments/docker/mita-amd64/dockerfile rename to mieru/deployments/docker/mita/dockerfile index dfd451ee8e..cf309c779b 100644 --- a/mieru/deployments/docker/mita-amd64/dockerfile +++ b/mieru/deployments/docker/mita/dockerfile @@ -1,22 +1,12 @@ FROM golang:1.20-alpine AS builder - RUN apk update && apk add --no-cache git - RUN git clone https://github.com/enfein/mieru.git /build - WORKDIR /build - -RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o mita cmd/mita/mita.go - +RUN GOOS=linux CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o mita cmd/mita/mita.go FROM alpine AS base - COPY --from=builder /build/mita /usr/local/bin/ - WORKDIR /app - COPY . . - RUN chmod +x ./start.sh && adduser -H -D -g "" mita && mkdir -p /etc/mita - CMD ["./start.sh"] diff --git a/mieru/deployments/docker/mita-amd64/start.sh b/mieru/deployments/docker/mita/start.sh similarity index 98% rename from mieru/deployments/docker/mita-amd64/start.sh rename to mieru/deployments/docker/mita/start.sh index a08fd87d38..d0298b9727 100755 --- a/mieru/deployments/docker/mita-amd64/start.sh +++ b/mieru/deployments/docker/mita/start.sh @@ -6,9 +6,7 @@ mita run & sleep 2 mita apply config ./conf/config.json - mita start - mita describe config wait -n diff --git a/mieru/pkg/protocol/const.go b/mieru/pkg/protocol/const.go index 246f2a4484..8871c621c5 100644 --- a/mieru/pkg/protocol/const.go +++ b/mieru/pkg/protocol/const.go @@ -13,8 +13,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//go:build !android - package protocol import "time" diff --git a/mieru/pkg/protocol/const_android.go b/mieru/pkg/protocol/const_android.go deleted file mode 100644 index 96955d8bb8..0000000000 --- a/mieru/pkg/protocol/const_android.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (C) 2024 mieru authors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//go:build android - -package protocol - -import "time" - -const ( - // tickInterval is the event trigger interval. - tickInterval = 5 * time.Millisecond -) diff --git a/mieru/pkg/protocol/session.go b/mieru/pkg/protocol/session.go index 567b63fb23..b811d0b7db 100644 --- a/mieru/pkg/protocol/session.go +++ b/mieru/pkg/protocol/session.go @@ -21,7 +21,6 @@ import ( "io" "math" "net" - "runtime" "sync" "sync/atomic" "time" @@ -579,7 +578,8 @@ func (s *Session) runOutputLoop(ctx context.Context) error { func (s *Session) runOutputOnceStream() { if s.outputHasErr.Load() { - runtime.Gosched() + // Can't run output. + time.Sleep(tickInterval) return } @@ -605,7 +605,8 @@ func (s *Session) runOutputOnceStream() { func (s *Session) runOutputOncePacket() { if s.outputHasErr.Load() { - runtime.Gosched() + // Can't run output. + time.Sleep(tickInterval) return } diff --git a/openwrt-packages/luci-theme-alpha/Makefile b/openwrt-packages/luci-theme-alpha/Makefile index 44174974a6..8f01a3ce9a 100644 --- a/openwrt-packages/luci-theme-alpha/Makefile +++ b/openwrt-packages/luci-theme-alpha/Makefile @@ -46,9 +46,13 @@ endef define Package/luci-theme-$(THEME_NAME)/postinst #!/bin/sh -[ -n "$${IPKG_INSTROOT}" ] || { - ( . /etc/uci-defaults/30-luci-theme-$(THEME_NAME) ) && rm -f /etc/uci-defaults/30-luci-theme-$(THEME_NAME) -} +if [ -z "$${IPKG_INSTROOT}" ]; then + if [ -f /etc/uci-defaults/30-luci-theme-$(THEME_NAME) ]; then + . /etc/uci-defaults/30-luci-theme-$(THEME_NAME) + rm -f /etc/uci-defaults/30-luci-theme-$(THEME_NAME) + fi +fi +exit 0 endef $(eval $(call BuildPackage,luci-theme-$(THEME_NAME))) diff --git a/openwrt-packages/luci-theme-alpha/README.md b/openwrt-packages/luci-theme-alpha/README.md index e251e3c4a3..179b6bb3f6 100644 --- a/openwrt-packages/luci-theme-alpha/README.md +++ b/openwrt-packages/luci-theme-alpha/README.md @@ -71,6 +71,9 @@ DBAI (Device Berbasis Arm Indonesia) this theme using bootstrap framework + vanilla css icons made by me + flaticons +### Attention +This theme required luci-theme-alpha-config installed + donate buy me a padang rice or coffee https://saweria.co/derisamedia diff --git a/openwrt-passwall/.github/workflows/Auto compile with openwrt sdk.yml b/openwrt-passwall/.github/workflows/Auto compile with openwrt sdk.yml index 711a2f8bc9..bbdf8c6422 100644 --- a/openwrt-passwall/.github/workflows/Auto compile with openwrt sdk.yml +++ b/openwrt-passwall/.github/workflows/Auto compile with openwrt sdk.yml @@ -273,7 +273,7 @@ jobs: echo "CONFIG_PACKAGE_luci-app-passwall=m" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_Iptables_Transparent_Proxy=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_Nftables_Transparent_Proxy=y" >> .config - echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_ChinaDNS_NG=y" >> .config + echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_Geoview=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_Haproxy=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_Hysteria=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_NaiveProxy=y" >> .config @@ -288,7 +288,6 @@ jobs: echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_Trojan_Plus=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_tuic_client=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_V2ray_Geodata=y" >> .config - echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_V2ray_Geoview=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_V2ray_Plugin=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_Xray=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_Xray_Plugin=y" >> .config diff --git a/openwrt-passwall/luci-app-passwall/Makefile b/openwrt-passwall/luci-app-passwall/Makefile index 6653f5f741..18cdd8de65 100644 --- a/openwrt-passwall/luci-app-passwall/Makefile +++ b/openwrt-passwall/luci-app-passwall/Makefile @@ -12,6 +12,7 @@ PKG_RELEASE:=1 PKG_CONFIG_DEPENDS:= \ CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Geoview \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy \ @@ -26,7 +27,6 @@ PKG_CONFIG_DEPENDS:= \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Trojan_Plus \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_tuic_client \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Geodata \ - CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Geoview \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Xray \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Xray_Plugin @@ -64,6 +64,11 @@ config PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy select PACKAGE_kmod-nft-nat default y if PACKAGE_firewall4 +config PACKAGE_$(PKG_NAME)_INCLUDE_Geoview + bool "Include Geoview" + select PACKAGE_geoview + default y if aarch64||arm||i386||x86_64 + config PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy bool "Include Haproxy" select PACKAGE_haproxy @@ -141,11 +146,6 @@ config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Geodata select PACKAGE_v2ray-geosite default n -config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Geoview - bool "Include V2ray_Geoview" - select PACKAGE_geoview - default y if aarch64||arm||i386||x86_64 - config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin bool "Include V2ray-Plugin (Shadowsocks Plugin)" select PACKAGE_v2ray-plugin diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua index c5ed6e2090..b32f240991 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua @@ -290,6 +290,9 @@ o:depends({ _tcp_node_bool = "1" }) o:value("dnsmasq", "Dnsmasq") o:value("chinadns-ng", translate("ChinaDNS-NG (recommended)")) +o = s:option(DummyValue, "view_chinadns_log", " ") +o.template = appname .. "/acl/view_chinadns_log" + o = s:option(Flag, "filter_proxy_ipv6", translate("Filter Proxy Host IPv6"), translate("Experimental feature.")) o.default = "0" o:depends({ _tcp_node_bool = "1" }) @@ -418,6 +421,4 @@ o:value("direct", translate("Direct DNS")) o.description = desc .. "" o:depends({dns_shunt = "dnsmasq", tcp_proxy_mode = "proxy", chn_list = "direct"}) -m:append(Template(appname .. "/acl/footer")) - return m diff --git a/openwrt-passwall/luci-app-passwall/luasrc/view/passwall/acl/footer.htm b/openwrt-passwall/luci-app-passwall/luasrc/view/passwall/acl/view_chinadns_log.htm similarity index 81% rename from openwrt-passwall/luci-app-passwall/luasrc/view/passwall/acl/footer.htm rename to openwrt-passwall/luci-app-passwall/luasrc/view/passwall/acl/view_chinadns_log.htm index 6026e6dc92..7c9933a08b 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/view/passwall/acl/footer.htm +++ b/openwrt-passwall/luci-app-passwall/luasrc/view/passwall/acl/view_chinadns_log.htm @@ -5,12 +5,6 @@ local api = require "luci.passwall.api" //' + "?flag=" + sid + "', '_blank')"); + logLink.setAttribute("onclick", "window.open('" + '<%=api.url("get_chinadns_log") .. "?flag=" .. section%>' + "', '_blank')"); select.insertAdjacentElement("afterend", logLink); } }, 1000); diff --git a/shadowsocks-rust/build/build-host-release.ps1 b/shadowsocks-rust/build/build-host-release.ps1 index b8cbd49120..0332377a6d 100644 --- a/shadowsocks-rust/build/build-host-release.ps1 +++ b/shadowsocks-rust/build/build-host-release.ps1 @@ -34,13 +34,15 @@ $Version = $Version -replace '"' $PackageReleasePath = "${PSScriptRoot}\release" $PackageName = "shadowsocks-v${Version}.${TargetTriple}.zip" $PackagePath = "${PackageReleasePath}\${PackageName}" +$ReleaseBuildPath = "${PSScriptRoot}\..\target\release" Write-Host $Version Write-Host $PackageReleasePath Write-Host $PackageName Write-Host $PackagePath +Write-Host $ReleaseBuildPath -Push-Location "${PSScriptRoot}\..\target\release" +Push-Location $ReleaseBuildPath $ProgressPreference = "SilentlyContinue" New-Item "${PackageReleasePath}" -ItemType Directory -ErrorAction SilentlyContinue @@ -48,7 +50,7 @@ $CompressParam = @{ LiteralPath = "sslocal.exe", "ssserver.exe", "ssurl.exe", "ssmanager.exe", "ssservice.exe" DestinationPath = "${PackagePath}" } -if ([System.IO.File]::Exists("sswinservice.exe")) { +if ([System.IO.File]::Exists("$ReleaseBuildPath\sswinservice.exe")) { $CompressParam.LiteralPath += "sswinservice.exe" } Compress-Archive @CompressParam @@ -60,3 +62,5 @@ $PackageHash = (Get-FileHash -Path "${PackagePath}" -Algorithm SHA256).Hash "${PackageHash} ${PackageName}" | Out-File -FilePath "${PackageChecksumPath}" Write-Host "Created release packet checksum ${PackageChecksumPath}" + +Pop-Location 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 961ef52a5a..372afc178f 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js @@ -174,9 +174,9 @@ return baseclass.extend({ ], rules_logical_payload_count: { - 'AND': 2, - 'OR': 2, - 'NOT': 1, + 'AND': { req: 2, opt: undefined }, + 'OR': { req: 2, opt: undefined }, + 'NOT': { req: 1, opt: 0 }, //'SUB-RULE': 0, }, diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js index b3650a921d..aca97cbdc4 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js @@ -125,13 +125,20 @@ class RulesEntry { return this.payload[n] || {}; } - setPayload(n, obj) { + getPayloads() { + return this.payload || []; + } + + setPayload(n, obj, limit) { this.payload[n] ||= {}; Object.keys(obj).forEach((key) => { this.payload[n][key] = obj[key] || null; }); + if (limit) + this.payload.splice(limit); + return this } @@ -152,7 +159,7 @@ class RulesEntry { var logical = hm.rules_logical_type.map(e => e[0] || e).includes(this.type), factor = ''; if (logical) { - let n = hm.rules_logical_payload_count[this.type] || 0; + let n = hm.rules_logical_payload_count[this.type] ? hm.rules_logical_payload_count[this.type].opt : 0; factor = '(%s)'.format(this.payload.slice(0, n).map((payload) => { return '(%s‚%s)'.format(payload.type || '', payload.factor || ''); }).join('ꓹ')); @@ -215,8 +222,35 @@ function renderPayload(s, total, uciconfig) { o.rmempty = false; o.modalonly = true; } + var initDynamicPayload = function(o, n, key, uciconfig) { + o.load = L.bind(function(n, key, uciconfig, section_id) { + return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayloads().slice(n).map(e => e[key]); + }, o, n, key, uciconfig); + o.validate = function(section_id, value) { + value = this.formvalue(section_id); + var UIEl = this.section.getUIElement(section_id, 'entry'); + var rule = new RulesEntry(UIEl.getValue()); + + let n = this.option.match(/^payload(\d+)_/)[1]; + let limit = rule.getPayloads().length; + value.forEach((val) => { + rule.setPayload(n, {factor: val}); n++; + }); + rule.setPayload(limit, {factor: null}, limit); + var newvalue = rule.toString(); + + UIEl.node.previousSibling.innerText = newvalue; + UIEl.setValue(newvalue); + + return true; + } + o.write = function() {}; + o.rmempty = true; + o.modalonly = true; + } var o, prefix; + // StaticList payload for (var n=0; n { - if (n < hm.rules_logical_payload_count[key]) + if (n < hm.rules_logical_payload_count[key].req) o.depends('type', key); }) initPayload(o, n, 'type', uciconfig); @@ -297,6 +331,68 @@ function renderPayload(s, total, uciconfig) { return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayload(n)[key]; }, o, n, 'factor', uciconfig) } + + // DynamicList payload + var extenbox = {}; + Object.entries(hm.rules_logical_payload_count).filter(e => e[1].opt === undefined).forEach((e) => { + let n = e[1].req; + if (!Array.isArray(extenbox[n])) + extenbox[n] = []; + extenbox[n].push(e[0]); + }) + Object.keys(extenbox).forEach((n) => { + prefix = `payload${n}_`; + + o = s.option(form.DynamicList, prefix + 'type', _('Type') + ' ++'); + o.default = hm.rules_type[0][0]; + hm.rules_type.forEach((res) => { + o.value.apply(o, res); + }) + extenbox[n].forEach((type) => { + o.depends('type', type); + }) + initDynamicPayload(o, n, 'type', uciconfig); + o.validate = function(section_id, value) { + value = this.formvalue(section_id); + var UIEl = this.section.getUIElement(section_id, 'entry'); + var rule = new RulesEntry(UIEl.getValue()); + + let n = this.option.match(/^payload(\d+)_/)[1]; + value.forEach((val) => { + rule.setPayload(n, {type: val}); n++; + }); + rule.setPayload(n, {factor: null}, n); + var newvalue = rule.toString(); + + UIEl.node.previousSibling.innerText = newvalue; + UIEl.setValue(newvalue); + + return true; + } + + o = s.option(form.DynamicList, prefix + 'fused', _('Factor') + ' ++', + _('Content will not be verified, Please make sure you enter it correctly.')); + o.value('', _('-- Please choose --')); + extenbox[n].forEach((type) => { + o.depends(Object.fromEntries([['type', type], [prefix + 'type', /.+/]])); + }) + initDynamicPayload(o, n, 'factor', uciconfig); + o.load = L.bind(function(n, key, uciconfig, section_id) { + let fusedval = [ + ['', _('-- Please choose --')], + ['NETWORK', '-- NETWORK --'], + ['udp', _('UDP')], + ['tcp', _('TCP')], + ['RULESET', '-- RULE-SET --'] + ]; + hm.loadRulesetLabel.call(this, null, section_id); + this.keylist = [...fusedval.map(e => e[0]), ...this.keylist]; + this.vallist = [...fusedval.map(e => e[1]), ...this.vallist]; + this.super('load', section_id); + + return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayloads().slice(n).map(e => e[key]); + }, o, n, 'factor', uciconfig) + }) } function renderRules(s, uciconfig) { @@ -356,7 +452,7 @@ function renderRules(s, uciconfig) { o.rmempty = false; o.modalonly = true; - renderPayload(s, Math.max(...Object.values(hm.rules_logical_payload_count)), uciconfig); + renderPayload(s, Math.max(...Object.values(hm.rules_logical_payload_count).map(e => e.req)), uciconfig); o = s.option(form.ListValue, 'detour', _('Proxy group')); o.renderWidget = function(/* ... */) { @@ -1018,7 +1114,7 @@ return view.extend({ so.rmempty = false; so.editable = true; - so = ss.option(form.ListValue, 'proxy', _('Proxy group'), + so = ss.option(form.ListValue, 'proxy', _('Proxy group override'), _('Override the Proxy group of DNS server.')); so.renderWidget = function(/* ... */) { var frameEl = form.ListValue.prototype.renderWidget.apply(this, arguments); diff --git a/small/luci-app-fchomo/po/templates/fchomo.pot b/small/luci-app-fchomo/po/templates/fchomo.pot index bb3246abcb..d2e1ff5d81 100644 --- a/small/luci-app-fchomo/po/templates/fchomo.pot +++ b/small/luci-app-fchomo/po/templates/fchomo.pot @@ -1667,6 +1667,10 @@ msgstr "" msgid "Proxy group" msgstr "" +#: htdocs/luci-static/resources/view/fchomo/client.js:1117 +msgid "Proxy group override" +msgstr "" + #: htdocs/luci-static/resources/view/fchomo/global.js:450 msgid "Proxy mode" msgstr "" diff --git a/small/luci-app-fchomo/po/zh_Hans/fchomo.po b/small/luci-app-fchomo/po/zh_Hans/fchomo.po index dd31714518..ee5035cddb 100644 --- a/small/luci-app-fchomo/po/zh_Hans/fchomo.po +++ b/small/luci-app-fchomo/po/zh_Hans/fchomo.po @@ -1690,6 +1690,10 @@ msgstr "代理链" msgid "Proxy group" msgstr "代理组" +#: htdocs/luci-static/resources/view/fchomo/client.js:1117 +msgid "Proxy group override" +msgstr "代理组覆盖" + #: htdocs/luci-static/resources/view/fchomo/global.js:450 msgid "Proxy mode" msgstr "代理模式" diff --git a/small/luci-app-fchomo/po/zh_Hant/fchomo.po b/small/luci-app-fchomo/po/zh_Hant/fchomo.po index 8d5dfd7e7b..7fef86cd19 100644 --- a/small/luci-app-fchomo/po/zh_Hant/fchomo.po +++ b/small/luci-app-fchomo/po/zh_Hant/fchomo.po @@ -1690,6 +1690,10 @@ msgstr "代理鏈" msgid "Proxy group" msgstr "代理組" +#: htdocs/luci-static/resources/view/fchomo/client.js:1117 +msgid "Proxy group override" +msgstr "代理組覆蓋" + #: htdocs/luci-static/resources/view/fchomo/global.js:450 msgid "Proxy mode" msgstr "代理模式" diff --git a/small/luci-app-passwall/Makefile b/small/luci-app-passwall/Makefile index 6653f5f741..18cdd8de65 100644 --- a/small/luci-app-passwall/Makefile +++ b/small/luci-app-passwall/Makefile @@ -12,6 +12,7 @@ PKG_RELEASE:=1 PKG_CONFIG_DEPENDS:= \ CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Geoview \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy \ @@ -26,7 +27,6 @@ PKG_CONFIG_DEPENDS:= \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Trojan_Plus \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_tuic_client \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Geodata \ - CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Geoview \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Xray \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Xray_Plugin @@ -64,6 +64,11 @@ config PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy select PACKAGE_kmod-nft-nat default y if PACKAGE_firewall4 +config PACKAGE_$(PKG_NAME)_INCLUDE_Geoview + bool "Include Geoview" + select PACKAGE_geoview + default y if aarch64||arm||i386||x86_64 + config PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy bool "Include Haproxy" select PACKAGE_haproxy @@ -141,11 +146,6 @@ config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Geodata select PACKAGE_v2ray-geosite default n -config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Geoview - bool "Include V2ray_Geoview" - select PACKAGE_geoview - default y if aarch64||arm||i386||x86_64 - config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin bool "Include V2ray-Plugin (Shadowsocks Plugin)" select PACKAGE_v2ray-plugin diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua index c5ed6e2090..b32f240991 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua @@ -290,6 +290,9 @@ o:depends({ _tcp_node_bool = "1" }) o:value("dnsmasq", "Dnsmasq") o:value("chinadns-ng", translate("ChinaDNS-NG (recommended)")) +o = s:option(DummyValue, "view_chinadns_log", " ") +o.template = appname .. "/acl/view_chinadns_log" + o = s:option(Flag, "filter_proxy_ipv6", translate("Filter Proxy Host IPv6"), translate("Experimental feature.")) o.default = "0" o:depends({ _tcp_node_bool = "1" }) @@ -418,6 +421,4 @@ o:value("direct", translate("Direct DNS")) o.description = desc .. "" o:depends({dns_shunt = "dnsmasq", tcp_proxy_mode = "proxy", chn_list = "direct"}) -m:append(Template(appname .. "/acl/footer")) - return m diff --git a/small/luci-app-passwall/luasrc/view/passwall/acl/footer.htm b/small/luci-app-passwall/luasrc/view/passwall/acl/view_chinadns_log.htm similarity index 81% rename from small/luci-app-passwall/luasrc/view/passwall/acl/footer.htm rename to small/luci-app-passwall/luasrc/view/passwall/acl/view_chinadns_log.htm index 6026e6dc92..7c9933a08b 100644 --- a/small/luci-app-passwall/luasrc/view/passwall/acl/footer.htm +++ b/small/luci-app-passwall/luasrc/view/passwall/acl/view_chinadns_log.htm @@ -5,12 +5,6 @@ local api = require "luci.passwall.api" //' + "?flag=" + sid + "', '_blank')"); + logLink.setAttribute("onclick", "window.open('" + '<%=api.url("get_chinadns_log") .. "?flag=" .. section%>' + "', '_blank')"); select.insertAdjacentElement("afterend", logLink); } }, 1000); diff --git a/small/sing-box/Makefile b/small/sing-box/Makefile index c924a18ad3..0bbd7e8e63 100644 --- a/small/sing-box/Makefile +++ b/small/sing-box/Makefile @@ -6,12 +6,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=sing-box -PKG_VERSION:=1.10.5 +PKG_VERSION:=1.10.6 PKG_RELEASE:=1 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)? -PKG_HASH:=ca0385b45d160c2c2a1d0e09665f4f04caac27cb3dd9d6132173316dfd873b75 +PKG_HASH:=bb3ca59c848f1509d0e183c54c91716993ce77ab0c9c48a0de0bcd4cc1f0e021 PKG_LICENSE:=GPL-3.0-or-later PKG_LICENSE_FILES:=LICENSE diff --git a/v2rayn/.github/workflows/build-linux.yml b/v2rayn/.github/workflows/build-linux.yml index 88b89d2a2d..913e88a9da 100644 --- a/v2rayn/.github/workflows/build-linux.yml +++ b/v2rayn/.github/workflows/build-linux.yml @@ -73,4 +73,5 @@ jobs: with: file: ${{ github.workspace }}/v2rayN*.zip tag: ${{ github.event.inputs.release_tag }} - file_glob: true \ No newline at end of file + file_glob: true + prerelease: true \ No newline at end of file diff --git a/v2rayn/.github/workflows/build-osx.yml b/v2rayn/.github/workflows/build-osx.yml index 369faabeeb..6a50d6b493 100644 --- a/v2rayn/.github/workflows/build-osx.yml +++ b/v2rayn/.github/workflows/build-osx.yml @@ -74,4 +74,5 @@ jobs: with: file: ${{ github.workspace }}/v2rayN*.zip tag: ${{ github.event.inputs.release_tag }} - file_glob: true \ No newline at end of file + file_glob: true + prerelease: true \ No newline at end of file diff --git a/v2rayn/.github/workflows/build-windows.yml b/v2rayn/.github/workflows/build-windows.yml index 2b19e6461b..97c4a997aa 100644 --- a/v2rayn/.github/workflows/build-windows.yml +++ b/v2rayn/.github/workflows/build-windows.yml @@ -64,4 +64,5 @@ jobs: with: file: ${{ github.workspace }}/v2rayN*.zip tag: ${{ github.event.inputs.release_tag }} - file_glob: true \ No newline at end of file + file_glob: true + prerelease: true \ No newline at end of file diff --git a/v2rayn/package-osx.sh b/v2rayn/package-osx.sh index cfbd83598d..6bd0d92294 100755 --- a/v2rayn/package-osx.sh +++ b/v2rayn/package-osx.sh @@ -11,16 +11,16 @@ echo "When this file exists, app will not store configs under this folder" > "$P chmod +x "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN" mkdir -p "$PackagePath/icons.iconset" -sips -z 16 16 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.png" --out "$PackagePath/icons.iconset/icon_16x16.png" -sips -z 32 32 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.png" --out "$PackagePath/icons.iconset/icon_16x16@2x.png" -sips -z 32 32 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.png" --out "$PackagePath/icons.iconset/icon_32x32.png" -sips -z 64 64 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.png" --out "$PackagePath/icons.iconset/icon_32x32@2x.png" -sips -z 128 128 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.png" --out "$PackagePath/icons.iconset/icon_128x128.png" -sips -z 256 256 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.png" --out "$PackagePath/icons.iconset/icon_128x128@2x.png" -sips -z 256 256 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.png" --out "$PackagePath/icons.iconset/icon_256x256.png" -sips -z 512 512 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.png" --out "$PackagePath/icons.iconset/icon_256x256@2x.png" -sips -z 512 512 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.png" --out "$PackagePath/icons.iconset/icon_512x512.png" -sips -z 1024 1024 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.png" --out "$PackagePath/icons.iconset/icon_512x512@2x.png" +sips -z 16 16 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN2.png" --out "$PackagePath/icons.iconset/icon_16x16.png" +sips -z 32 32 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN2.png" --out "$PackagePath/icons.iconset/icon_16x16@2x.png" +sips -z 32 32 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN2.png" --out "$PackagePath/icons.iconset/icon_32x32.png" +sips -z 64 64 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN2.png" --out "$PackagePath/icons.iconset/icon_32x32@2x.png" +sips -z 128 128 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN2.png" --out "$PackagePath/icons.iconset/icon_128x128.png" +sips -z 256 256 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN2.png" --out "$PackagePath/icons.iconset/icon_128x128@2x.png" +sips -z 256 256 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN2.png" --out "$PackagePath/icons.iconset/icon_256x256.png" +sips -z 512 512 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN2.png" --out "$PackagePath/icons.iconset/icon_256x256@2x.png" +sips -z 512 512 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN2.png" --out "$PackagePath/icons.iconset/icon_512x512.png" +sips -z 1024 1024 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN2.png" --out "$PackagePath/icons.iconset/icon_512x512@2x.png" iconutil -c icns "$PackagePath/icons.iconset" -o "$PackagePath/v2rayN.app/Contents/Resources/AppIcon.icns" cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF diff --git a/v2rayn/v2rayN/AmazTool/Program.cs b/v2rayn/v2rayN/AmazTool/Program.cs index f81f24be16..cbfc13322e 100644 --- a/v2rayn/v2rayN/AmazTool/Program.cs +++ b/v2rayn/v2rayN/AmazTool/Program.cs @@ -15,8 +15,15 @@ return; } - var fileName = Uri.UnescapeDataString(string.Join(" ", args)); - UpgradeApp.Upgrade(fileName); + var argData = Uri.UnescapeDataString(string.Join(" ", args)); + if (argData.Equals("rebootas")) + { + Thread.Sleep(1000); + Utils.StartV2RayN(); + return; + } + + UpgradeApp.Upgrade(argData); } } } \ No newline at end of file diff --git a/v2rayn/v2rayN/AmazTool/UpgradeApp.cs b/v2rayn/v2rayN/AmazTool/UpgradeApp.cs index b633c877a5..81154d52dc 100644 --- a/v2rayn/v2rayN/AmazTool/UpgradeApp.cs +++ b/v2rayn/v2rayN/AmazTool/UpgradeApp.cs @@ -21,11 +21,11 @@ namespace AmazTool Console.WriteLine(Resx.Resource.TryTerminateProcess); try { - var existing = Process.GetProcessesByName(V2rayN); + var existing = Process.GetProcessesByName(Utils.V2rayN); foreach (var pp in existing) { var path = pp.MainModule?.FileName ?? ""; - if (path.StartsWith(GetPath(V2rayN))) + if (path.StartsWith(Utils.GetPath(Utils.V2rayN))) { pp?.Kill(); pp?.WaitForExit(1000); @@ -42,7 +42,7 @@ namespace AmazTool StringBuilder sb = new(); try { - string thisAppOldFile = $"{GetExePath()}.tmp"; + string thisAppOldFile = $"{Utils.GetExePath()}.tmp"; File.Delete(thisAppOldFile); string splitKey = "/"; @@ -62,12 +62,12 @@ namespace AmazTool if (lst.Length == 1) continue; string fullName = string.Join(splitKey, lst[1..lst.Length]); - if (string.Equals(GetExePath(), GetPath(fullName), StringComparison.OrdinalIgnoreCase)) + if (string.Equals(Utils.GetExePath(), Utils.GetPath(fullName), StringComparison.OrdinalIgnoreCase)) { - File.Move(GetExePath(), thisAppOldFile); + File.Move(Utils.GetExePath(), thisAppOldFile); } - string entryOutputPath = GetPath(fullName); + string entryOutputPath = Utils.GetPath(fullName); Directory.CreateDirectory(Path.GetDirectoryName(entryOutputPath)!); entry.ExtractToFile(entryOutputPath, true); @@ -92,39 +92,11 @@ namespace AmazTool Console.WriteLine(Resx.Resource.Restartv2rayN); Waiting(2); - Process process = new() - { - StartInfo = new() - { - UseShellExecute = true, - FileName = V2rayN, - WorkingDirectory = StartupPath() - } - }; - process.Start(); + + Utils.StartV2RayN(); } - private static string GetExePath() - { - return Environment.ProcessPath ?? Process.GetCurrentProcess().MainModule?.FileName ?? string.Empty; - } - - private static string StartupPath() - { - return AppDomain.CurrentDomain.BaseDirectory; - } - - private static string GetPath(string fileName) - { - string startupPath = StartupPath(); - if (string.IsNullOrEmpty(fileName)) - { - return startupPath; - } - return Path.Combine(startupPath, fileName); - } - - private static void Waiting(int second) + public static void Waiting(int second) { for (var i = second; i > 0; i--) { @@ -132,7 +104,5 @@ namespace AmazTool Thread.Sleep(1000); } } - - private static string V2rayN => "v2rayN"; } } \ No newline at end of file diff --git a/v2rayn/v2rayN/AmazTool/Utils.cs b/v2rayn/v2rayN/AmazTool/Utils.cs new file mode 100644 index 0000000000..9152679f90 --- /dev/null +++ b/v2rayn/v2rayN/AmazTool/Utils.cs @@ -0,0 +1,43 @@ +using System.Diagnostics; + +namespace AmazTool +{ + internal class Utils + { + public static string GetExePath() + { + return Environment.ProcessPath ?? Process.GetCurrentProcess().MainModule?.FileName ?? string.Empty; + } + + public static string StartupPath() + { + return AppDomain.CurrentDomain.BaseDirectory; + } + + public static string GetPath(string fileName) + { + string startupPath = StartupPath(); + if (string.IsNullOrEmpty(fileName)) + { + return startupPath; + } + return Path.Combine(startupPath, fileName); + } + + public static string V2rayN => "v2rayN"; + + public static void StartV2RayN() + { + Process process = new() + { + StartInfo = new() + { + UseShellExecute = true, + FileName = V2rayN, + WorkingDirectory = StartupPath() + } + }; + process.Start(); + } + } +} \ No newline at end of file diff --git a/v2rayn/v2rayN/ServiceLib/Common/ProcUtils.cs b/v2rayn/v2rayN/ServiceLib/Common/ProcUtils.cs new file mode 100644 index 0000000000..495167cb4b --- /dev/null +++ b/v2rayn/v2rayN/ServiceLib/Common/ProcUtils.cs @@ -0,0 +1,105 @@ +using System.Diagnostics; + +namespace ServiceLib.Common; + +public static class ProcUtils +{ + private static readonly string _tag = "ProcUtils"; + + public static void ProcessStart(string? fileName, string arguments = "") + { + ProcessStart(fileName, arguments, null); + } + + public static int? ProcessStart(string? fileName, string arguments, string? dir) + { + if (fileName.IsNullOrEmpty()) + { + return null; + } + try + { + if (fileName.Contains(' ')) fileName = fileName.AppendQuotes(); + if (arguments.Contains(' ')) arguments = arguments.AppendQuotes(); + + Process process = new() + { + StartInfo = new ProcessStartInfo + { + UseShellExecute = true, + FileName = fileName, + Arguments = arguments, + WorkingDirectory = dir + } + }; + process.Start(); + return process.Id; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return null; + } + + public static void RebootAsAdmin(bool blAdmin = true) + { + try + { + ProcessStartInfo startInfo = new() + { + UseShellExecute = true, + Arguments = Global.RebootAs, + WorkingDirectory = Utils.StartupPath(), + FileName = Utils.GetExePath().AppendQuotes(), + Verb = blAdmin ? "runas" : null, + }; + Process.Start(startInfo); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + } + + public static async Task ProcessKill(int pid) + { + try + { + await ProcessKill(Process.GetProcessById(pid), false); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + } + + public static async Task ProcessKill(Process? proc, bool review) + { + if (proc is null) + { + return; + } + + var fileName = review ? proc?.MainModule?.FileName : null; + var processName = review ? proc?.ProcessName : null; + + try { proc?.Kill(true); } catch (Exception ex) { Logging.SaveLog(_tag, ex); } + try { proc?.Kill(); } catch (Exception ex) { Logging.SaveLog(_tag, ex); } + try { proc?.Close(); } catch (Exception ex) { Logging.SaveLog(_tag, ex); } + try { proc?.Dispose(); } catch (Exception ex) { Logging.SaveLog(_tag, ex); } + + await Task.Delay(300); + if (review && fileName != null) + { + var proc2 = Process.GetProcessesByName(processName) + .FirstOrDefault(t => t.MainModule?.FileName == fileName); + if (proc2 != null) + { + Logging.SaveLog($"{_tag}, KillProcess not completing the job"); + await ProcessKill(proc2, false); + proc2 = null; + } + } + } +} \ No newline at end of file diff --git a/v2rayn/v2rayN/ServiceLib/Common/Utils.cs b/v2rayn/v2rayN/ServiceLib/Common/Utils.cs index 5985fa9f0c..15023f2b04 100644 --- a/v2rayn/v2rayN/ServiceLib/Common/Utils.cs +++ b/v2rayn/v2rayN/ServiceLib/Common/Utils.cs @@ -517,10 +517,10 @@ namespace ServiceLib.Common #region 杂项 - public static bool UpgradeAppExists(out string fileName) + public static bool UpgradeAppExists(out string upgradeFileName) { - fileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, GetExeName("AmazTool")); - return File.Exists(fileName); + upgradeFileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, GetExeName("AmazTool")); + return File.Exists(upgradeFileName); } /// @@ -591,26 +591,6 @@ namespace ServiceLib.Common return Guid.TryParse(strSrc, out _); } - public static void ProcessStart(string? fileName, string arguments = "") - { - try - { - if (fileName.IsNullOrEmpty()) - { - return; - } - - if (fileName.Contains(' ')) fileName = fileName.AppendQuotes(); - if (arguments.Contains(' ')) arguments = arguments.AppendQuotes(); - - Process.Start(new ProcessStartInfo(fileName, arguments) { UseShellExecute = true }); - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - } - public static Dictionary GetSystemHosts() { var systemHosts = new Dictionary(); diff --git a/v2rayn/v2rayN/ServiceLib/Handler/CoreHandler.cs b/v2rayn/v2rayN/ServiceLib/Handler/CoreHandler.cs index 8c5b46ca6f..32451ba1d1 100644 --- a/v2rayn/v2rayN/ServiceLib/Handler/CoreHandler.cs +++ b/v2rayn/v2rayN/ServiceLib/Handler/CoreHandler.cs @@ -32,9 +32,9 @@ namespace ServiceLib.Handler { if (it.CoreType == ECoreType.v2rayN) { - if (Utils.UpgradeAppExists(out var fileName)) + if (Utils.UpgradeAppExists(out var upgradeFileName)) { - await Utils.SetLinuxChmod(fileName); + await Utils.SetLinuxChmod(upgradeFileName); } continue; } @@ -55,7 +55,7 @@ namespace ServiceLib.Handler { if (node == null) { - ShowMsg(false, ResUI.CheckServerSettings); + UpdateFunc(false, ResUI.CheckServerSettings); return; } @@ -63,13 +63,13 @@ namespace ServiceLib.Handler var result = await CoreConfigHandler.GenerateClientConfig(node, fileName); if (result.Success != true) { - ShowMsg(true, result.Msg); + UpdateFunc(true, result.Msg); return; } - ShowMsg(true, $"{node.GetSummary()}"); - ShowMsg(false, $"{Utils.GetRuntimeInfo()}"); - ShowMsg(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"))); + UpdateFunc(true, $"{node.GetSummary()}"); + UpdateFunc(false, $"{Utils.GetRuntimeInfo()}"); + UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"))); await CoreStop(); await Task.Delay(100); await CoreStart(node); @@ -81,15 +81,23 @@ namespace ServiceLib.Handler var coreType = selecteds.Exists(t => t.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.WireGuard) ? ECoreType.sing_box : ECoreType.Xray; var configPath = Utils.GetConfigPath(Global.CoreSpeedtestConfigFileName); var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, configPath, selecteds, coreType); - ShowMsg(false, result.Msg); + UpdateFunc(false, result.Msg); if (result.Success != true) { return -1; } - ShowMsg(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"))); - ShowMsg(false, configPath); - return await CoreStartSpeedtest(configPath, coreType); + UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"))); + UpdateFunc(false, configPath); + + var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(coreType); + var proc = await RunProcess(coreInfo, Global.CoreSpeedtestConfigFileName, true, false); + if (proc is null) + { + return -1; + } + + return proc.Id; } public async Task CoreStop() @@ -98,13 +106,13 @@ namespace ServiceLib.Handler { if (_process != null) { - await KillProcess(_process, true); + await ProcUtils.ProcessKill(_process, true); _process = null; } if (_processPre != null) { - await KillProcess(_processPre, true); + await ProcUtils.ProcessKill(_processPre, true); _processPre = null; } @@ -120,41 +128,8 @@ namespace ServiceLib.Handler } } - public async Task CoreStopPid(int pid) - { - try - { - await KillProcess(Process.GetProcessById(pid), false); - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - } - #region Private - private string CoreFindExe(CoreInfo coreInfo) - { - var fileName = string.Empty; - foreach (var name in coreInfo.CoreExes) - { - var vName = Utils.GetBinPath(Utils.GetExeName(name), coreInfo.CoreType.ToString()); - if (File.Exists(vName)) - { - fileName = vName; - break; - } - } - if (Utils.IsNullOrEmpty(fileName)) - { - var msg = string.Format(ResUI.NotFoundCore, Utils.GetBinPath("", coreInfo.CoreType.ToString()), string.Join(", ", coreInfo.CoreExes.ToArray()), coreInfo.Url); - Logging.SaveLog(msg); - ShowMsg(false, msg); - } - return fileName; - } - private async Task CoreStart(ProfileItem node) { var coreType = _config.RunningCoreType = AppHandler.Instance.GetCoreType(node, node.ConfigType); @@ -194,28 +169,7 @@ namespace ServiceLib.Handler } } - private async Task CoreStartSpeedtest(string configPath, ECoreType coreType) - { - try - { - var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(coreType); - var proc = await RunProcess(coreInfo, Global.CoreSpeedtestConfigFileName, true, false); - if (proc is null) - { - return -1; - } - - return proc.Id; - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - ShowMsg(false, ex.Message); - return -1; - } - } - - private void ShowMsg(bool notify, string msg) + private void UpdateFunc(bool notify, string msg) { _updateFunc?.Invoke(notify, msg); } @@ -233,11 +187,12 @@ namespace ServiceLib.Handler #region Process - private async Task RunProcess(CoreInfo coreInfo, string configPath, bool displayLog, bool mayNeedSudo) + private async Task RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo) { - var fileName = CoreFindExe(coreInfo); + var fileName = CoreInfoHandler.Instance.GetCoreExecFile(coreInfo, out var msg); if (Utils.IsNullOrEmpty(fileName)) { + UpdateFunc(false, msg); return null; } @@ -272,12 +227,12 @@ namespace ServiceLib.Handler proc.OutputDataReceived += (sender, e) => { if (Utils.IsNullOrEmpty(e.Data)) return; - ShowMsg(false, e.Data + Environment.NewLine); + UpdateFunc(false, e.Data + Environment.NewLine); }; proc.ErrorDataReceived += (sender, e) => { if (Utils.IsNullOrEmpty(e.Data)) return; - ShowMsg(false, e.Data + Environment.NewLine); + UpdateFunc(false, e.Data + Environment.NewLine); if (!startUpSuccessful) { @@ -319,40 +274,11 @@ namespace ServiceLib.Handler catch (Exception ex) { Logging.SaveLog(_tag, ex); - ShowMsg(true, ex.Message); + UpdateFunc(true, ex.Message); return null; } } - private async Task KillProcess(Process? proc, bool review) - { - if (proc is null) - { - return; - } - - var fileName = proc?.MainModule?.FileName; - var processName = proc?.ProcessName; - - try { proc?.Kill(true); } catch (Exception ex) { Logging.SaveLog(_tag, ex); } - try { proc?.Kill(); } catch (Exception ex) { Logging.SaveLog(_tag, ex); } - try { proc?.Close(); } catch (Exception ex) { Logging.SaveLog(_tag, ex); } - try { proc?.Dispose(); } catch (Exception ex) { Logging.SaveLog(_tag, ex); } - - await Task.Delay(500); - if (review) - { - var proc2 = Process.GetProcessesByName(processName) - .FirstOrDefault(t => t.MainModule?.FileName == fileName); - if (proc2 != null) - { - Logging.SaveLog($"{_tag}, KillProcess not completing the job"); - await KillProcess(proc2, false); - proc2 = null; - } - } - } - #endregion Process #region Linux diff --git a/v2rayn/v2rayN/ServiceLib/Handler/CoreInfoHandler.cs b/v2rayn/v2rayN/ServiceLib/Handler/CoreInfoHandler.cs index d8399b06a3..7a947e623f 100644 --- a/v2rayn/v2rayN/ServiceLib/Handler/CoreInfoHandler.cs +++ b/v2rayn/v2rayN/ServiceLib/Handler/CoreInfoHandler.cs @@ -29,6 +29,27 @@ return _coreInfo ?? []; } + public string GetCoreExecFile(CoreInfo? coreInfo, out string msg) + { + var fileName = string.Empty; + msg = string.Empty; + foreach (var name in coreInfo?.CoreExes) + { + var vName = Utils.GetBinPath(Utils.GetExeName(name), coreInfo.CoreType.ToString()); + if (File.Exists(vName)) + { + fileName = vName; + break; + } + } + if (fileName.IsNullOrEmpty()) + { + msg = string.Format(ResUI.NotFoundCore, Utils.GetBinPath("", coreInfo.CoreType.ToString()), string.Join(", ", coreInfo.CoreExes.ToArray()), coreInfo.Url); + Logging.SaveLog(msg); + } + return fileName; + } + private void InitCoreInfo() { _coreInfo = []; diff --git a/v2rayn/v2rayN/ServiceLib/Sample/dns_v2ray_normal b/v2rayn/v2rayN/ServiceLib/Sample/dns_v2ray_normal index add15f19cb..9aecbd6e94 100644 --- a/v2rayn/v2rayN/ServiceLib/Sample/dns_v2ray_normal +++ b/v2rayn/v2rayN/ServiceLib/Sample/dns_v2ray_normal @@ -4,6 +4,15 @@ "proxy.example.com": "127.0.0.1" }, "servers": [ + { + "address": "1.1.1.1", + "domains": [ + "geosite:geolocation-!cn" + ], + "expectIPs": [ + "geoip:!cn" + ] + }, { "address": "223.5.5.5", "domains": [ @@ -13,7 +22,6 @@ "geoip:cn" ] }, - "1.1.1.1", "8.8.8.8", "https://dns.google/dns-query" ] diff --git a/v2rayn/v2rayN/ServiceLib/ServiceLib.csproj b/v2rayn/v2rayN/ServiceLib/ServiceLib.csproj index 70484187f7..b6efdb58bd 100644 --- a/v2rayn/v2rayN/ServiceLib/ServiceLib.csproj +++ b/v2rayn/v2rayN/ServiceLib/ServiceLib.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 7.5.1 + 7.5.2 diff --git a/v2rayn/v2rayN/ServiceLib/Services/SpeedtestService.cs b/v2rayn/v2rayN/ServiceLib/Services/SpeedtestService.cs index 05a5ffd76c..cf134e0188 100644 --- a/v2rayn/v2rayN/ServiceLib/Services/SpeedtestService.cs +++ b/v2rayn/v2rayN/ServiceLib/Services/SpeedtestService.cs @@ -216,7 +216,7 @@ namespace ServiceLib.Services { if (pid > 0) { - await CoreHandler.Instance.CoreStopPid(pid); + await ProcUtils.ProcessKill(pid); } await ProfileExHandler.Instance.SaveTo(); } @@ -278,7 +278,7 @@ namespace ServiceLib.Services if (pid > 0) { - await CoreHandler.Instance.CoreStopPid(pid); + await ProcUtils.ProcessKill(pid); } await ProfileExHandler.Instance.SaveTo(); } @@ -342,7 +342,7 @@ namespace ServiceLib.Services if (pid > 0) { - await CoreHandler.Instance.CoreStopPid(pid); + await ProcUtils.ProcessKill(pid); } await ProfileExHandler.Instance.SaveTo(); } diff --git a/v2rayn/v2rayN/ServiceLib/ViewModels/AddServer2ViewModel.cs b/v2rayn/v2rayN/ServiceLib/ViewModels/AddServer2ViewModel.cs index c0cfb32cea..df84eef734 100644 --- a/v2rayn/v2rayN/ServiceLib/ViewModels/AddServer2ViewModel.cs +++ b/v2rayn/v2rayN/ServiceLib/ViewModels/AddServer2ViewModel.cs @@ -103,7 +103,7 @@ namespace ServiceLib.ViewModels address = Utils.GetConfigPath(address); if (File.Exists(address)) { - Utils.ProcessStart(address); + ProcUtils.ProcessStart(address); } else { diff --git a/v2rayn/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs b/v2rayn/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs index 7723077059..cbf6beb3d3 100644 --- a/v2rayn/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs +++ b/v2rayn/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs @@ -130,11 +130,6 @@ namespace ServiceLib.ViewModels DisplayOperationMsg(ResUI.LocalRestoreInvalidZipTips); return; } - if (!Utils.UpgradeAppExists(out _)) - { - DisplayOperationMsg(ResUI.UpgradeAppNotExistTip); - return; - } //backup first var fileBackup = Utils.GetBackupPath(BackupFileName); @@ -150,12 +145,16 @@ namespace ServiceLib.ViewModels if (Utils.IsWindows()) { - service?.RebootAsAdmin(false); + ProcUtils.RebootAsAdmin(false); } else { - service?.Shutdown(); + if (Utils.UpgradeAppExists(out var upgradeFileName)) + { + ProcUtils.ProcessStart(upgradeFileName, Global.RebootAs, Utils.StartupPath()); + } } + service?.Shutdown(); } else { diff --git a/v2rayn/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs b/v2rayn/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs index e8def23f17..929ea1eb72 100644 --- a/v2rayn/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs +++ b/v2rayn/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs @@ -1,9 +1,7 @@ using ReactiveUI; using ReactiveUI.Fody.Helpers; using Splat; -using System.Diagnostics; using System.Reactive; -using System.Reactive.Linq; namespace ServiceLib.ViewModels { @@ -312,27 +310,16 @@ namespace ServiceLib.ViewModels public async Task UpgradeApp(string arg) { - if (!Utils.UpgradeAppExists(out var fileName)) + if (!Utils.UpgradeAppExists(out var upgradeFileName)) { NoticeHandler.Instance.SendMessageAndEnqueue(ResUI.UpgradeAppNotExistTip); Logging.SaveLog("UpgradeApp does not exist"); return; } - Process process = new() + var id = ProcUtils.ProcessStart(upgradeFileName, arg, Utils.StartupPath()); + if (id > 0) { - StartInfo = new ProcessStartInfo - { - UseShellExecute = true, - FileName = fileName, - Arguments = arg.AppendQuotes(), - WorkingDirectory = Utils.StartupPath() - } - }; - process.Start(); - if (process.Id > 0) - { - await MyAppExitAsync(false); await MyAppExitAsync(false); } } @@ -513,22 +500,10 @@ namespace ServiceLib.ViewModels } } - public async Task RebootAsAdmin(bool blAdmin = true) + public async Task RebootAsAdmin() { - try - { - ProcessStartInfo startInfo = new() - { - UseShellExecute = true, - Arguments = Global.RebootAs, - WorkingDirectory = Utils.StartupPath(), - FileName = Utils.GetExePath().AppendQuotes(), - Verb = blAdmin ? "runas" : null, - }; - Process.Start(startInfo); - await MyAppExitAsync(false); - } - catch { } + ProcUtils.RebootAsAdmin(); + await MyAppExitAsync(false); } private async Task ClearServerStatistics() @@ -542,15 +517,15 @@ namespace ServiceLib.ViewModels var path = Utils.StartupPath(); if (Utils.IsWindows()) { - Utils.ProcessStart(path); + ProcUtils.ProcessStart(path); } else if (Utils.IsLinux()) { - Utils.ProcessStart("nautilus", path); + ProcUtils.ProcessStart("nautilus", path); } else if (Utils.IsOSX()) { - Utils.ProcessStart("open", path); + ProcUtils.ProcessStart("open", path); } } diff --git a/v2rayn/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs b/v2rayn/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs index 56fed37d23..55a12ac077 100644 --- a/v2rayn/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs +++ b/v2rayn/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs @@ -65,12 +65,12 @@ namespace v2rayN.Desktop.Views private void linkDnsObjectDoc_Click(object? sender, RoutedEventArgs e) { - Utils.ProcessStart("https://xtls.github.io/config/dns.html#dnsobject"); + ProcUtils.ProcessStart("https://xtls.github.io/config/dns.html#dnsobject"); } private void linkDnsSingboxObjectDoc_Click(object? sender, RoutedEventArgs e) { - Utils.ProcessStart("https://sing-box.sagernet.org/zh/configuration/dns/"); + ProcUtils.ProcessStart("https://sing-box.sagernet.org/zh/configuration/dns/"); } } } \ No newline at end of file diff --git a/v2rayn/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs b/v2rayn/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs index 52c0fdc9ec..e73fda5d26 100644 --- a/v2rayn/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs +++ b/v2rayn/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs @@ -330,12 +330,12 @@ namespace v2rayN.Desktop.Views private void menuPromotion_Click(object? sender, RoutedEventArgs e) { - Utils.ProcessStart($"{Utils.Base64Decode(Global.PromotionUrl)}?t={DateTime.Now.Ticks}"); + ProcUtils.ProcessStart($"{Utils.Base64Decode(Global.PromotionUrl)}?t={DateTime.Now.Ticks}"); } private void menuSettingsSetUWP_Click(object? sender, RoutedEventArgs e) { - Utils.ProcessStart(Utils.GetBinPath("EnableLoopback.exe")); + ProcUtils.ProcessStart(Utils.GetBinPath("EnableLoopback.exe")); } public async Task ScanScreenTaskAsync() @@ -481,7 +481,7 @@ namespace v2rayN.Desktop.Views { if (sender is MenuItem item) { - Utils.ProcessStart(item.Tag?.ToString()); + ProcUtils.ProcessStart(item.Tag?.ToString()); } } diff --git a/v2rayn/v2rayN/v2rayN.Desktop/Views/RoutingRuleDetailsWindow.axaml.cs b/v2rayn/v2rayN/v2rayN.Desktop/Views/RoutingRuleDetailsWindow.axaml.cs index 3c2c45088a..732ae00e97 100644 --- a/v2rayn/v2rayN/v2rayN.Desktop/Views/RoutingRuleDetailsWindow.axaml.cs +++ b/v2rayn/v2rayN/v2rayN.Desktop/Views/RoutingRuleDetailsWindow.axaml.cs @@ -95,7 +95,7 @@ namespace v2rayN.Desktop.Views private void linkRuleobjectDoc_Click(object? sender, RoutedEventArgs e) { - Utils.ProcessStart("https://xtls.github.io/config/routing.html#ruleobject"); + ProcUtils.ProcessStart("https://xtls.github.io/config/routing.html#ruleobject"); } } } \ No newline at end of file diff --git a/v2rayn/v2rayN/v2rayN.Desktop/Views/RoutingRuleSettingWindow.axaml.cs b/v2rayn/v2rayN/v2rayN.Desktop/Views/RoutingRuleSettingWindow.axaml.cs index deeba10d6e..d38f185f18 100644 --- a/v2rayn/v2rayN/v2rayN.Desktop/Views/RoutingRuleSettingWindow.axaml.cs +++ b/v2rayn/v2rayN/v2rayN.Desktop/Views/RoutingRuleSettingWindow.axaml.cs @@ -203,7 +203,7 @@ namespace v2rayN.Desktop.Views private void linkCustomRulesetPath4Singbox(object? sender, RoutedEventArgs e) { - Utils.ProcessStart("https://github.com/2dust/v2rayCustomRoutingList/blob/master/singbox_custom_ruleset_example.json"); + ProcUtils.ProcessStart("https://github.com/2dust/v2rayCustomRoutingList/blob/master/singbox_custom_ruleset_example.json"); } } } \ No newline at end of file diff --git a/v2rayn/v2rayN/v2rayN.Desktop/Views/RoutingSettingWindow.axaml.cs b/v2rayn/v2rayN/v2rayN.Desktop/Views/RoutingSettingWindow.axaml.cs index 15f5dd2f70..749feebc82 100644 --- a/v2rayn/v2rayN/v2rayN.Desktop/Views/RoutingSettingWindow.axaml.cs +++ b/v2rayn/v2rayN/v2rayN.Desktop/Views/RoutingSettingWindow.axaml.cs @@ -117,12 +117,12 @@ namespace v2rayN.Desktop.Views private void linkdomainStrategy_Click(object? sender, RoutedEventArgs e) { - Utils.ProcessStart("https://xtls.github.io/config/routing.html"); + ProcUtils.ProcessStart("https://xtls.github.io/config/routing.html"); } private void linkdomainStrategy4Singbox_Click(object? sender, RoutedEventArgs e) { - Utils.ProcessStart("https://sing-box.sagernet.org/zh/configuration/shared/listen/#domain_strategy"); + ProcUtils.ProcessStart("https://sing-box.sagernet.org/zh/configuration/shared/listen/#domain_strategy"); } private void btnCancel_Click(object? sender, RoutedEventArgs e) diff --git a/v2rayn/v2rayN/v2rayN.Desktop/v2rayN.Desktop.csproj b/v2rayn/v2rayN/v2rayN.Desktop/v2rayN.Desktop.csproj index bc95f8c6ac..abe7aa5b95 100644 --- a/v2rayn/v2rayN/v2rayN.Desktop/v2rayN.Desktop.csproj +++ b/v2rayn/v2rayN/v2rayN.Desktop/v2rayN.Desktop.csproj @@ -48,6 +48,9 @@ Always + + Always + diff --git a/v2rayn/v2rayN/v2rayN.Desktop/v2rayN2.png b/v2rayn/v2rayN/v2rayN.Desktop/v2rayN2.png new file mode 100644 index 0000000000..d869057f83 Binary files /dev/null and b/v2rayn/v2rayN/v2rayN.Desktop/v2rayN2.png differ diff --git a/v2rayn/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs b/v2rayn/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs index 290021e0a9..56a8242d57 100644 --- a/v2rayn/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs +++ b/v2rayn/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs @@ -66,12 +66,12 @@ namespace v2rayN.Views private void linkDnsObjectDoc_Click(object sender, RoutedEventArgs e) { - Utils.ProcessStart("https://xtls.github.io/config/dns.html#dnsobject"); + ProcUtils.ProcessStart("https://xtls.github.io/config/dns.html#dnsobject"); } private void linkDnsSingboxObjectDoc_Click(object sender, RoutedEventArgs e) { - Utils.ProcessStart("https://sing-box.sagernet.org/zh/configuration/dns/"); + ProcUtils.ProcessStart("https://sing-box.sagernet.org/zh/configuration/dns/"); } } } \ No newline at end of file diff --git a/v2rayn/v2rayN/v2rayN/Views/MainWindow.xaml.cs b/v2rayn/v2rayN/v2rayN/Views/MainWindow.xaml.cs index c2e745f3b2..d91656a363 100644 --- a/v2rayn/v2rayN/v2rayN/Views/MainWindow.xaml.cs +++ b/v2rayn/v2rayN/v2rayN/Views/MainWindow.xaml.cs @@ -308,12 +308,12 @@ namespace v2rayN.Views private void menuPromotion_Click(object sender, RoutedEventArgs e) { - Utils.ProcessStart($"{Utils.Base64Decode(Global.PromotionUrl)}?t={DateTime.Now.Ticks}"); + ProcUtils.ProcessStart($"{Utils.Base64Decode(Global.PromotionUrl)}?t={DateTime.Now.Ticks}"); } private void menuSettingsSetUWP_Click(object sender, RoutedEventArgs e) { - Utils.ProcessStart(Utils.GetBinPath("EnableLoopback.exe")); + ProcUtils.ProcessStart(Utils.GetBinPath("EnableLoopback.exe")); } private async Task ScanScreenTaskAsync() @@ -443,7 +443,7 @@ namespace v2rayN.Views { if (sender is MenuItem item) { - Utils.ProcessStart(item.Tag.ToString()); + ProcUtils.ProcessStart(item.Tag.ToString()); } } diff --git a/v2rayn/v2rayN/v2rayN/Views/RoutingRuleDetailsWindow.xaml.cs b/v2rayn/v2rayN/v2rayN/Views/RoutingRuleDetailsWindow.xaml.cs index 7262e2b79e..5b6cca4c3e 100644 --- a/v2rayn/v2rayN/v2rayN/Views/RoutingRuleDetailsWindow.xaml.cs +++ b/v2rayn/v2rayN/v2rayN/Views/RoutingRuleDetailsWindow.xaml.cs @@ -89,7 +89,7 @@ namespace v2rayN.Views private void linkRuleobjectDoc_Click(object sender, RoutedEventArgs e) { - Utils.ProcessStart("https://xtls.github.io/config/routing.html#ruleobject"); + ProcUtils.ProcessStart("https://xtls.github.io/config/routing.html#ruleobject"); } } } \ No newline at end of file diff --git a/v2rayn/v2rayN/v2rayN/Views/RoutingRuleSettingWindow.xaml.cs b/v2rayn/v2rayN/v2rayN/Views/RoutingRuleSettingWindow.xaml.cs index 64342617f9..4340b2f3b1 100644 --- a/v2rayn/v2rayN/v2rayN/Views/RoutingRuleSettingWindow.xaml.cs +++ b/v2rayn/v2rayN/v2rayN/Views/RoutingRuleSettingWindow.xaml.cs @@ -197,7 +197,7 @@ namespace v2rayN.Views private void linkCustomRulesetPath4Singbox(object sender, RoutedEventArgs e) { - Utils.ProcessStart("https://github.com/2dust/v2rayCustomRoutingList/blob/master/singbox_custom_ruleset_example.json"); + ProcUtils.ProcessStart("https://github.com/2dust/v2rayCustomRoutingList/blob/master/singbox_custom_ruleset_example.json"); } } } \ No newline at end of file diff --git a/v2rayn/v2rayN/v2rayN/Views/RoutingSettingWindow.xaml.cs b/v2rayn/v2rayN/v2rayN/Views/RoutingSettingWindow.xaml.cs index 37b1c38a3b..39086ec453 100644 --- a/v2rayn/v2rayN/v2rayN/Views/RoutingSettingWindow.xaml.cs +++ b/v2rayn/v2rayN/v2rayN/Views/RoutingSettingWindow.xaml.cs @@ -122,12 +122,12 @@ namespace v2rayN.Views private void linkdomainStrategy_Click(object sender, System.Windows.RoutedEventArgs e) { - Utils.ProcessStart("https://xtls.github.io/config/routing.html"); + ProcUtils.ProcessStart("https://xtls.github.io/config/routing.html"); } private void linkdomainStrategy4Singbox_Click(object sender, RoutedEventArgs e) { - Utils.ProcessStart("https://sing-box.sagernet.org/zh/configuration/shared/listen/#domain_strategy"); + ProcUtils.ProcessStart("https://sing-box.sagernet.org/zh/configuration/shared/listen/#domain_strategy"); } private void btnCancel_Click(object sender, System.Windows.RoutedEventArgs e) diff --git a/v2rayng/.github/workflows/build.yml b/v2rayng/.github/workflows/build.yml index 336464baf9..0d5c829099 100644 --- a/v2rayng/.github/workflows/build.yml +++ b/v2rayng/.github/workflows/build.yml @@ -17,6 +17,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + submodules: 'true' - name: Prepare build dir run: | @@ -62,7 +64,6 @@ jobs: cd ${{ github.workspace }}/build/AndroidLibV2rayLite bash compile-tun2socks.sh tar -xvzf libtun2socks.so.tgz - cp -r libs/* ${{ github.workspace }}/V2rayNG/app/libs/ env: NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} @@ -75,7 +76,7 @@ jobs: - name: Copy libtun2socks run: | - cp -r ${{ github.workspace }}/build/AndroidLibV2rayLite/libs/* ${{ github.workspace }}/V2rayNG/app/libs/ + cp -r ${{ github.workspace }}/build/AndroidLibV2rayLite/libs ${{ github.workspace }}/V2rayNG/app - name: Download libv2ray uses: robinraju/release-downloader@v1 @@ -85,6 +86,44 @@ jobs: fileName: 'libv2ray.aar' out-file-path: V2rayNG/app/libs/ + - name: Restore cached libhysteria2 + id: cache-libhysteria2-restore + uses: actions/cache/restore@v4 + with: + path: ${{ github.workspace }}/hysteria/libs + key: libhysteria2-${{ runner.os }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria.sh') }} + + - name: Fetch Go version from AndroidLibXrayLite + if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true' + run: | + GO_VERSION=$(curl -sL https://github.com/2dust/AndroidLibXrayLite/raw/refs/heads/main/go.mod | sed -n -E 's/.*go ([0-9]+\.[0-9]+\.[0-9]+).*/\1/p') + echo "Go version: $GO_VERSION" + echo "GO_VERSION=$GO_VERSION" >> $GITHUB_ENV + + - name: Setup Golang + if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true' + uses: actions/setup-go@v5 + with: + go-version: '${{ env.GO_VERSION }}' + + - name: Build libhysteria2 + if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true' + run: | + bash libhysteria2.sh + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + + - name: Save libhysteria2 + if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ${{ github.workspace }}/hysteria/libs + key: libhysteria2-${{ runner.os }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria.sh') }} + + - name: Copy libhysteria2 + run: | + cp -r ${{ github.workspace }}/hysteria/libs ${{ github.workspace }}/V2rayNG/app + - name: Setup Java uses: actions/setup-java@v4 with: diff --git a/v2rayng/.gitignore b/v2rayng/.gitignore index 239dde7f7d..b844412dc8 100644 --- a/v2rayng/.gitignore +++ b/v2rayng/.gitignore @@ -3,5 +3,4 @@ V2rayNG/app/release/output.json .idea/ .gradle/ -libtun2socks.so -libhysteria2.so \ No newline at end of file +*.so diff --git a/v2rayng/.gitmodules b/v2rayng/.gitmodules new file mode 100644 index 0000000000..b0a1e97d3e --- /dev/null +++ b/v2rayng/.gitmodules @@ -0,0 +1,3 @@ +[submodule "hysteria"] + path = hysteria + url = https://github.com/apernet/hysteria diff --git a/v2rayng/V2rayNG/app/libs/arm64-v8a/libhysteria2.so b/v2rayng/V2rayNG/app/libs/arm64-v8a/libhysteria2.so deleted file mode 100644 index 9de1a37725..0000000000 Binary files a/v2rayng/V2rayNG/app/libs/arm64-v8a/libhysteria2.so and /dev/null differ diff --git a/v2rayng/V2rayNG/app/libs/armeabi-v7a/libhysteria2.so b/v2rayng/V2rayNG/app/libs/armeabi-v7a/libhysteria2.so deleted file mode 100644 index ac5e737982..0000000000 Binary files a/v2rayng/V2rayNG/app/libs/armeabi-v7a/libhysteria2.so and /dev/null differ diff --git a/v2rayng/V2rayNG/app/libs/x86/libhysteria2.so b/v2rayng/V2rayNG/app/libs/x86/libhysteria2.so deleted file mode 100644 index 1a6eb668ab..0000000000 Binary files a/v2rayng/V2rayNG/app/libs/x86/libhysteria2.so and /dev/null differ diff --git a/v2rayng/V2rayNG/app/libs/x86_64/libhysteria2.so b/v2rayng/V2rayNG/app/libs/x86_64/libhysteria2.so deleted file mode 100644 index 4ae0497e68..0000000000 Binary files a/v2rayng/V2rayNG/app/libs/x86_64/libhysteria2.so and /dev/null differ diff --git a/v2rayng/hysteria/.github/FUNDING.yml b/v2rayng/hysteria/.github/FUNDING.yml new file mode 100644 index 0000000000..2977cd7a26 --- /dev/null +++ b/v2rayng/hysteria/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: [ 'https://v2.hysteria.network/docs/Donation/' ] diff --git a/v2rayng/hysteria/.github/ISSUE_TEMPLATE/bug_report.md b/v2rayng/hysteria/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..4123105236 --- /dev/null +++ b/v2rayng/hysteria/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,26 @@ +--- +name: Bug report +about: Report anything you think is a bug and needs to be fixed. +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Logs** +Attach logs from the client/server when the error occurs. + +**Device and Operating System** +What are you using it on. + +**Additional context** +Add any other context about the problem here. diff --git a/v2rayng/hysteria/.github/ISSUE_TEMPLATE/bug_report.zh.md b/v2rayng/hysteria/.github/ISSUE_TEMPLATE/bug_report.zh.md new file mode 100644 index 0000000000..7edd6e8449 --- /dev/null +++ b/v2rayng/hysteria/.github/ISSUE_TEMPLATE/bug_report.zh.md @@ -0,0 +1,26 @@ +--- +name: Bug 反馈 +about: 反馈任何你认为是 bug 需要修复的问题。 +title: '' +labels: bug +assignees: '' + +--- + +**描述问题** +请尽量清晰精准地描述你遇到的问题。 + +**如何复现** +复现问题的步骤。 + +**预期行为** +你认为修复后的行为应该是怎样的。 + +**日志** +附上客户端/服务器端在错误发生前后的日志。 + +**设备和操作系统** +你在用什么设备和操作系统。 + +**额外信息** +其他你认为有助于解决问题的信息。 diff --git a/v2rayng/hysteria/.github/ISSUE_TEMPLATE/feature_request.md b/v2rayng/hysteria/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..107576c9a4 --- /dev/null +++ b/v2rayng/hysteria/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project. +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/v2rayng/hysteria/.github/ISSUE_TEMPLATE/feature_request.zh.md b/v2rayng/hysteria/.github/ISSUE_TEMPLATE/feature_request.zh.md new file mode 100644 index 0000000000..6c9311cde4 --- /dev/null +++ b/v2rayng/hysteria/.github/ISSUE_TEMPLATE/feature_request.zh.md @@ -0,0 +1,20 @@ +--- +name: 功能请求 +about: 为这个项目提出改进意见。 +title: '' +labels: enhancement +assignees: '' + +--- + +**你的功能请求是否与某个问题有关?** +请尽量清晰精准地描述你遇到的问题。例如:我家运营商限制 UDP 协议速度,导致 Hysteria 很慢,希望增加 FakeTCP 支持。 + +**描述你希望的解决方案** +请尽量清晰精准地描述你希望的解决方案。 + +**有没有其他替代方案** +请尽量清晰精准地描述你认为可能的替代方案。 + +**额外信息** +其他你认为有助于开发者了解你需求的信息。 diff --git a/v2rayng/hysteria/.github/dependabot.yml b/v2rayng/hysteria/.github/dependabot.yml new file mode 100644 index 0000000000..5445036e78 --- /dev/null +++ b/v2rayng/hysteria/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" \ No newline at end of file diff --git a/v2rayng/hysteria/.github/workflows/autotag.yaml b/v2rayng/hysteria/.github/workflows/autotag.yaml new file mode 100644 index 0000000000..a066a74b51 --- /dev/null +++ b/v2rayng/hysteria/.github/workflows/autotag.yaml @@ -0,0 +1,104 @@ +name: "Create release tags for nested modules" + +on: + push: + tags: + - app/v*.*.* + +permissions: + contents: write + +jobs: + tag: + name: "Create tags" + runs-on: ubuntu-latest + steps: + - name: "Extract tagbase" + id: extract_tagbase + uses: actions/github-script@v7 + with: + script: | + const ref = context.ref; + core.info(`context.ref: ${ref}`); + const refPrefix = 'refs/tags/app/'; + if (!ref.startsWith(refPrefix)) { + core.setFailed(`context.ref does not start with ${refPrefix}: ${ref}`); + return; + } + const tagbase = ref.slice(refPrefix.length); + core.info(`tagbase: ${tagbase}`); + core.setOutput('tagbase', tagbase); + + - name: "Tagging core/*" + uses: actions/github-script@v7 + env: + INPUT_TAGPREFIX: "core/" + INPUT_TAGBASE: ${{ steps.extract_tagbase.outputs.tagbase }} + with: + script: | + const tagbase = core.getInput('tagbase', { required: true }); + const tagprefix = core.getInput('tagprefix', { required: true }); + const refname = `tags/${tagprefix}${tagbase}`; + core.info(`creating ref ${refname}`); + try { + await github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `refs/${refname}`, + sha: context.sha + }); + core.info(`created ref ${refname}`); + return; + } catch (error) { + core.info(`failed to create ref ${refname}: ${error}`); + } + core.info(`updating ref ${refname}`) + try { + await github.rest.git.updateRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: refname, + sha: context.sha + }); + core.info(`updated ref ${refname}`); + return; + } catch (error) { + core.setFailed(`failed to update ref ${refname}: ${error}`); + } + + - name: "Tagging extras/*" + uses: actions/github-script@v7 + env: + INPUT_TAGPREFIX: "extras/" + INPUT_TAGBASE: ${{ steps.extract_tagbase.outputs.tagbase }} + with: + script: | + const tagbase = core.getInput('tagbase', { required: true }); + const tagprefix = core.getInput('tagprefix', { required: true }); + const refname = `tags/${tagprefix}${tagbase}`; + core.info(`creating ref ${refname}`); + try { + await github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `refs/${refname}`, + sha: context.sha + }); + core.info(`created ref ${refname}`); + return; + } catch (error) { + core.info(`failed to create ref ${refname}: ${error}`); + } + core.info(`updating ref ${refname}`) + try { + await github.rest.git.updateRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: refname, + sha: context.sha + }); + core.info(`updated ref ${refname}`); + return; + } catch (error) { + core.setFailed(`failed to update ref ${refname}: ${error}`); + } diff --git a/v2rayng/hysteria/.github/workflows/docker.yml b/v2rayng/hysteria/.github/workflows/docker.yml new file mode 100644 index 0000000000..daa00759fa --- /dev/null +++ b/v2rayng/hysteria/.github/workflows/docker.yml @@ -0,0 +1,44 @@ +name: "Build Docker Image" + +on: + push: + tags: + - app/v*.*.* + +jobs: + docker: + runs-on: ubuntu-latest + env: + ACTIONS_ALLOW_UNSECURE_COMMANDS: true + + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Get version + id: get_version + run: echo "version=$(git describe --tags --always --match 'app/v*' | sed -n 's|app/\([^/-]*\)\(-.*\)\{0,1\}|\1|p')" >> $GITHUB_OUTPUT + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v5 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: tobyxdd/hysteria:latest,tobyxdd/hysteria:v2,tobyxdd/hysteria:${{ steps.get_version.outputs.version }} + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/v2rayng/hysteria/.github/workflows/master.yml b/v2rayng/hysteria/.github/workflows/master.yml new file mode 100644 index 0000000000..2b852fc3f2 --- /dev/null +++ b/v2rayng/hysteria/.github/workflows/master.yml @@ -0,0 +1,52 @@ +name: "Build master branch" + +on: + push: + branches: + - master + +jobs: + build: + name: Build + runs-on: ubuntu-latest + env: + ACTIONS_ALLOW_UNSECURE_COMMANDS: true + + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: "1.23" + + - name: Setup Python # This is for the build script + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: r26b + add-to-path: false + + - name: Run build script + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + run: | + export HY_APP_PLATFORMS=$(sed 's/\r$//' platforms.txt | awk '!/^#/ && !/^$/' | paste -sd ",") + python hyperbole.py build -r + + - name: Generate hashes + run: | + for file in build/*; do + sha256sum $file >> build/hashes.txt + done + + - name: Archive + uses: actions/upload-artifact@v4 + with: + name: hysteria-master-${{ github.sha }} + path: build diff --git a/v2rayng/hysteria/.github/workflows/release.yml b/v2rayng/hysteria/.github/workflows/release.yml new file mode 100644 index 0000000000..d7a243351c --- /dev/null +++ b/v2rayng/hysteria/.github/workflows/release.yml @@ -0,0 +1,71 @@ +name: "Build release" + +on: + push: + tags: + - app/v*.*.* + +jobs: + build: + name: Build + runs-on: ubuntu-latest + env: + ACTIONS_ALLOW_UNSECURE_COMMANDS: true + + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Get version + id: get_version + run: echo "version=$(git describe --tags --always --match 'app/v*' | sed -n 's|app/\([^/-]*\)\(-.*\)\{0,1\}|\1|p')" >> $GITHUB_OUTPUT + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: "1.23" + + - name: Setup Python # This is for the build script + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: r26b + add-to-path: false + + - name: Run build script + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + run: | + export HY_APP_PLATFORMS=$(sed 's/\r$//' platforms.txt | awk '!/^#/ && !/^$/' | paste -sd ",") + python hyperbole.py build -r + + - name: Generate hashes + run: | + for file in build/*; do + sha256sum $file >> build/hashes.txt + done + + - name: Upload GitHub + uses: softprops/action-gh-release@v2 + with: + files: build/* + + - name: Upload CF bucket + uses: shallwefootball/upload-s3-action@v1.3.3 + with: + aws_key_id: ${{ secrets.CF_KEY_ID }} + aws_secret_access_key: ${{ secrets.CF_KEY }} + aws_bucket: "hydownload" + endpoint: "https://bea223c61d5a41250d127bd67f51dfec.r2.cloudflarestorage.com/" + source_dir: "build" + destination_dir: "app/${{ steps.get_version.outputs.version }}" + + - name: Publish to API + run: | + export HY_API_POST_KEY=${{ secrets.HY2_API_POST_KEY }} + pip install requests + python hyperbole.py publish diff --git a/v2rayng/hysteria/.github/workflows/scripts.yml b/v2rayng/hysteria/.github/workflows/scripts.yml new file mode 100644 index 0000000000..6e38848b9a --- /dev/null +++ b/v2rayng/hysteria/.github/workflows/scripts.yml @@ -0,0 +1,29 @@ +name: "Publish scripts" + +on: + push: + branches: + - master + paths: + - scripts/** + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + deployments: write + name: Publish scripts to Cloudflare Pages + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Publish to Cloudflare Pages + uses: cloudflare/pages-action@v1 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + projectName: hy2scripts + directory: scripts + gitHubToken: ${{ secrets.GITHUB_TOKEN }} + branch: main diff --git a/v2rayng/hysteria/.gitignore b/v2rayng/hysteria/.gitignore new file mode 100644 index 0000000000..958549b2ad --- /dev/null +++ b/v2rayng/hysteria/.gitignore @@ -0,0 +1,470 @@ +# Created by https://www.toptal.com/developers/gitignore/api/goland+all,intellij+all,go,windows,linux,macos,python,pycharm+all +# Edit at https://www.toptal.com/developers/gitignore?templates=goland+all,intellij+all,go,windows,linux,macos,python,pycharm+all + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +### GoLand+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### GoLand+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff + +# AWS User-specific + +# Generated files + +# Sensitive or high-churn files + +# Gradle + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake + +# Mongo Explorer plugin + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin + +# SonarLint plugin + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client + +# Android studio 3.1+ serialized cache file + +### Intellij+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + + + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### PyCharm+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff + +# AWS User-specific + +# Generated files + +# Sensitive or high-churn files + +# Gradle + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake + +# Mongo Explorer plugin + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin + +# SonarLint plugin + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client + +# Android studio 3.1+ serialized cache file + +### PyCharm+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + + + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/goland+all,intellij+all,go,windows,linux,macos,python,pycharm+all \ No newline at end of file diff --git a/v2rayng/hysteria/CHANGELOG.md b/v2rayng/hysteria/CHANGELOG.md new file mode 100644 index 0000000000..6f7a98070c --- /dev/null +++ b/v2rayng/hysteria/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +https://v2.hysteria.network/docs/Changelog/ diff --git a/v2rayng/hysteria/Dockerfile b/v2rayng/hysteria/Dockerfile new file mode 100644 index 0000000000..e49ad63cda --- /dev/null +++ b/v2rayng/hysteria/Dockerfile @@ -0,0 +1,39 @@ +FROM golang:1-alpine AS builder + +# GOPROXY is disabled by default, use: +# docker build --build-arg GOPROXY="https://goproxy.io" ... +# to enable GOPROXY. +ARG GOPROXY="" + +ENV GOPROXY ${GOPROXY} + +COPY . /go/src/github.com/apernet/hysteria + +WORKDIR /go/src/github.com/apernet/hysteria + +RUN set -ex \ + && apk add git build-base bash python3 \ + && python hyperbole.py build -r \ + && mv ./build/hysteria-* /go/bin/hysteria + +# multi-stage builds to create the final image +FROM alpine AS dist + +# set up nsswitch.conf for Go's "netgo" implementation +# - https://github.com/golang/go/blob/go1.9.1/src/net/conf.go#L194-L275 +# - docker run --rm debian:stretch grep '^hosts:' /etc/nsswitch.conf +RUN if [ ! -e /etc/nsswitch.conf ]; then echo 'hosts: files dns' > /etc/nsswitch.conf; fi + +# bash is used for debugging, tzdata is used to add timezone information. +# Install ca-certificates to ensure no CA certificate errors. +# +# Do not try to add the "--no-cache" option when there are multiple "apk" +# commands, this will cause the build process to become very slow. +RUN set -ex \ + && apk upgrade \ + && apk add bash tzdata ca-certificates \ + && rm -rf /var/cache/apk/* + +COPY --from=builder /go/bin/hysteria /usr/local/bin/hysteria + +ENTRYPOINT ["hysteria"] \ No newline at end of file diff --git a/v2rayng/hysteria/LICENSE.md b/v2rayng/hysteria/LICENSE.md new file mode 100644 index 0000000000..208e8f29bd --- /dev/null +++ b/v2rayng/hysteria/LICENSE.md @@ -0,0 +1,7 @@ +Copyright 2023 Toby + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/v2rayng/hysteria/PROTOCOL.md b/v2rayng/hysteria/PROTOCOL.md new file mode 100644 index 0000000000..835c2e9bfb --- /dev/null +++ b/v2rayng/hysteria/PROTOCOL.md @@ -0,0 +1,153 @@ +# Hysteria 2 Protocol Specification + +Hysteria is a TCP & UDP proxy based on QUIC, designed for speed, security and censorship resistance. This document describes the protocol used by Hysteria starting with version 2.0.0, sometimes internally referred to as the "v4" protocol. From here on, we will call it "the protocol" or "the Hysteria protocol". + +## Requirements Language + +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://tools.ietf.org/html/rfc2119). + +## Underlying Protocol & Wire Format + +The Hysteria protocol MUST be implemented on top of the standard QUIC transport protocol [RFC 9000](https://datatracker.ietf.org/doc/html/rfc9000) with [Unreliable Datagram Extension](https://datatracker.ietf.org/doc/rfc9221/). + +All multibyte numbers use Big Endian format. + +All variable-length integers ("varints") are encoded/decoded as defined in QUIC (RFC 9000). + +## Authentication & HTTP/3 masquerading + +One of the key features of the Hysteria protocol is that to a third party without proper authentication credentials (whether it's a middleman or an active prober), a Hysteria proxy server behaves just like a standard HTTP/3 web server. Additionally, the encrypted traffic between the client and the server appears indistinguishable from normal HTTP/3 traffic. + +Therefore, a Hysteria server MUST implement an HTTP/3 server (as defined by [RFC 9114](https://datatracker.ietf.org/doc/rfc9114/)) and handle HTTP requests as any standard web server would. To prevent active probers from detecting common response patterns in Hysteria servers, implementations SHOULD advise users to either host actual content or set it up as a reverse proxy for other sites. + +An actual Hysteria client, upon connection, MUST send the following HTTP/3 request to the server: + +``` +:method: POST +:path: /auth +:host: hysteria +Hysteria-Auth: [string] +Hysteria-CC-RX: [uint] +Hysteria-Padding: [string] +``` + +`Hysteria-Auth`: Authentication credentials. + +`Hysteria-CC-RX`: Client's maximum receive rate in bytes per second. A value of 0 indicates unknown. + +`Hysteria-Padding`: A random padding string of variable length. + +The Hysteria server MUST identify this special request, and, instead of attempting to serve content or forwarding it to an upstream site, it MUST authenticate the client using the provided information. If authentication is successful, the server MUST send the following response (HTTP status code 233): + +``` +:status: 233 HyOK +Hysteria-UDP: [true/false] +Hysteria-CC-RX: [uint/"auto"] +Hysteria-Padding: [string] +``` + +`Hysteria-UDP`: Whether the server supports UDP relay. + +`Hysteria-CC-RX`: Server's maximum receive rate in bytes per second. A value of 0 indicates unlimited; "auto" indicates the server refuses to provide a value and ask the client to use congestion control to determine the rate on its own. + +`Hysteria-Padding`: A random padding string of variable length. + +See the Congestion Control section for more information on how to use the `Hysteria-CC-RX` values. + +`Hysteria-Padding` is optional and is only intended to obfuscate the request/response pattern. It SHOULD be ignored by both sides. + +If authentication fails, the server MUST either act like a standard web server that does not understand the request, or in the case of being a reverse proxy, forward the request to the upstream site and return the response to the client. + +The client MUST check the status code to determine if the authentication was successful. If the status code is anything other than 233, the client MUST consider authentication to have failed and disconnect from the server. + +After (and only after) a client passes authentication, the server MUST consider this QUIC connection to be a Hysteria proxy connection. It MUST then start processing proxy requests from the client as described in the next section. + +## Proxy Requests + +### TCP + +For each TCP connection, the client MUST create a new QUIC bidirectional stream and send the following TCPRequest message: + +``` +[varint] 0x401 (TCPRequest ID) +[varint] Address length +[bytes] Address string (host:port) +[varint] Padding length +[bytes] Random padding +``` + +The server MUST respond with a TCPResponse message: + +``` +[uint8] Status (0x00 = OK, 0x01 = Error) +[varint] Message length +[bytes] Message string +[varint] Padding length +[bytes] Random padding +``` + +If the status is OK, the server MUST then begin forwarding data between the client and the specified TCP address until either side closes the connection. If the status is Error, the server MUST close the QUIC stream. + +### UDP + +UDP packets MUST be encapsulated in the following UDPMessage format and sent over QUIC's unreliable datagram (for both client-to-server and server-to-client): + +``` +[uint32] Session ID +[uint16] Packet ID +[uint8] Fragment ID +[uint8] Fragment count +[varint] Address length +[bytes] Address string (host:port) +[bytes] Payload +``` + +The client MUST use a unique Session ID for each UDP session. The server SHOULD assign a unique UDP port to each Session ID, unless it has another mechanism to differentiate packets from different sessions (e.g., symmetric NAT, varying outbound IP addresses, etc.). + +The protocol does not provide an explicit way to close a UDP session. While a client can retain and reuse a Session ID indefinitely, the server SHOULD release and reassign the port associated with the Session ID after a period of inactivity or some other criteria. If the client sends a UDP packet to a Session ID that is no longer recognized by the server, the server MUST treat it as a new session and assign a new port. + +If a server does not support UDP relay, it SHOULD silently discard all UDP messages received from the client. + +#### Fragmentation + +Due to the limit imposed by QUIC's unreliable datagram channel, any UDP packet that exceeds QUIC's maximum datagram size MUST either be fragmented or discarded. + +For fragmented packets, each fragment MUST carry the same unique Packet ID. The Fragment ID, starting from 0, indicates the index out of the total Fragment Count. Both the server and client MUST wait for all fragments of a fragmented packet to arrive before processing them. If one or more fragments of a packet are lost, the entire packet MUST be discarded. + +For packets that are not fragmented, the Fragment Count MUST be set to 1. In this case, the values of Packet ID and Fragment ID are irrelevant. + +## Congestion Control + +A unique feature of Hysteria is the ability to set the tx/rx (upload/download) rate on the client side. During authentication, the client sends its rx rate to the server via the `Hysteria-CC-RX` header. The server can use this to determine its transmission rate to the client, and vice versa by returning its rx rate to the client through the same header. + +Three special cases are: + +- If the client sends 0, it doesn't know its own rx rate. The server MUST use a congestion control algorithm (e.g., BBR, Cubic) to adjust its transmission rate. +- If the server responds with 0, it has no bandwidth limit. The client MAY transmit at any rate it wants. +- If the server responds with "auto", it chooses not to specify a rate. The client MUST use a congestion control algorithm to adjust its transmission rate. + +## "Salamander" Obfuscation + +The Hysteria protocol supports an optional obfuscation layer codenamed "Salamander". + +"Salamander" encapsulates all QUIC packets in the following format: + +``` +[8 bytes] Salt +[bytes] Payload +``` + +For each QUIC packet, the obfuscator MUST calculate the BLAKE2b-256 hash of a randomly generated 8-byte salt appended to a user-provided pre-shared key. + +``` +hash = BLAKE2b-256(key + salt) +``` + +The hash is then used to obfuscate the payload using the following algorithm: + +``` +for i in range(0, len(payload)): + payload[i] ^= hash[i % 32] +``` + +The deobfuscator MUST use the same algorithms to calculate the salted hash and deobfuscate the payload. Any invalid packet MUST be discarded. diff --git a/v2rayng/hysteria/README.md b/v2rayng/hysteria/README.md new file mode 100644 index 0000000000..94fa3353e9 --- /dev/null +++ b/v2rayng/hysteria/README.md @@ -0,0 +1,60 @@ +# ![Hysteria 2](logo.svg) + +[![License][1]][2] [![Release][3]][4] [![Telegram][5]][6] [![Discussions][7]][8] + +[1]: https://img.shields.io/badge/license-MIT-blue +[2]: LICENSE.md +[3]: https://img.shields.io/github/v/release/apernet/hysteria?style=flat-square +[4]: https://github.com/apernet/hysteria/releases +[5]: https://img.shields.io/badge/chat-Telegram-blue?style=flat-square +[6]: https://t.me/hysteria_github +[7]: https://img.shields.io/github/discussions/apernet/hysteria?style=flat-square +[8]: https://github.com/apernet/hysteria/discussions + +

Hysteria is a powerful, lightning fast and censorship resistant proxy.

+ +### [Get Started](https://v2.hysteria.network/) + +### [中文文档](https://v2.hysteria.network/zh/) + +### [Hysteria 1.x (legacy)](https://v1.hysteria.network/) + +--- + +
+
+

🛠️ Jack of all trades

+

Wide range of modes including SOCKS5, HTTP Proxy, TCP/UDP Forwarding, Linux TProxy, TUN - with more features being added constantly.

+
+ +
+

⚡ Blazing fast

+

Powered by a customized QUIC protocol, Hysteria is designed to deliver unparalleled performance over unreliable and lossy networks.

+
+ +
+

✊ Censorship resistant

+

The protocol masquerades as standard HTTP/3 traffic, making it very difficult for censors to detect and block without widespread collateral damage.

+
+ +
+

💻 Cross-platform

+

We have builds for every major platform and architecture. Deploy anywhere & use everywhere. Not to mention the long list of 3rd party apps.

+
+ +
+

🔗 Easy integration

+

With built-in support for custom authentication, traffic statistics & access control, Hysteria is easy to integrate into your infrastructure.

+
+ +
+

🤗 Chill and supportive

+

We have well-documented specifications and code for developers to contribute and/or build their own apps. And a helpful community, too.

+
+
+ +--- + +**If you find Hysteria useful, consider giving it a ⭐️!** + +[![Star History Chart](https://api.star-history.com/svg?repos=apernet/hysteria&type=Date)](https://star-history.com/#apernet/hysteria&Date) diff --git a/v2rayng/hysteria/app/cmd/client.go b/v2rayng/hysteria/app/cmd/client.go new file mode 100644 index 0000000000..65bc7e0c71 --- /dev/null +++ b/v2rayng/hysteria/app/cmd/client.go @@ -0,0 +1,1031 @@ +package cmd + +import ( + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "errors" + "fmt" + "net" + "net/netip" + "os" + "os/signal" + "runtime" + "slices" + "strconv" + "strings" + "syscall" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/zap" + + "github.com/apernet/hysteria/app/v2/internal/forwarding" + "github.com/apernet/hysteria/app/v2/internal/http" + "github.com/apernet/hysteria/app/v2/internal/proxymux" + "github.com/apernet/hysteria/app/v2/internal/redirect" + "github.com/apernet/hysteria/app/v2/internal/sockopts" + "github.com/apernet/hysteria/app/v2/internal/socks5" + "github.com/apernet/hysteria/app/v2/internal/tproxy" + "github.com/apernet/hysteria/app/v2/internal/tun" + "github.com/apernet/hysteria/app/v2/internal/url" + "github.com/apernet/hysteria/app/v2/internal/utils" + "github.com/apernet/hysteria/core/v2/client" + "github.com/apernet/hysteria/extras/v2/correctnet" + "github.com/apernet/hysteria/extras/v2/obfs" + "github.com/apernet/hysteria/extras/v2/transport/udphop" +) + +// Client flags +var ( + showQR bool +) + +var clientCmd = &cobra.Command{ + Use: "client", + Short: "Client mode", + Run: runClient, +} + +func init() { + initClientFlags() + rootCmd.AddCommand(clientCmd) +} + +func initClientFlags() { + clientCmd.Flags().BoolVar(&showQR, "qr", false, "show QR code for server config sharing") +} + +type clientConfig struct { + Server string `mapstructure:"server"` + Auth string `mapstructure:"auth"` + Transport clientConfigTransport `mapstructure:"transport"` + Obfs clientConfigObfs `mapstructure:"obfs"` + TLS clientConfigTLS `mapstructure:"tls"` + QUIC clientConfigQUIC `mapstructure:"quic"` + Bandwidth clientConfigBandwidth `mapstructure:"bandwidth"` + FastOpen bool `mapstructure:"fastOpen"` + Lazy bool `mapstructure:"lazy"` + SOCKS5 *socks5Config `mapstructure:"socks5"` + HTTP *httpConfig `mapstructure:"http"` + TCPForwarding []tcpForwardingEntry `mapstructure:"tcpForwarding"` + UDPForwarding []udpForwardingEntry `mapstructure:"udpForwarding"` + TCPTProxy *tcpTProxyConfig `mapstructure:"tcpTProxy"` + UDPTProxy *udpTProxyConfig `mapstructure:"udpTProxy"` + TCPRedirect *tcpRedirectConfig `mapstructure:"tcpRedirect"` + TUN *tunConfig `mapstructure:"tun"` +} + +type clientConfigTransportUDP struct { + HopInterval time.Duration `mapstructure:"hopInterval"` +} + +type clientConfigTransport struct { + Type string `mapstructure:"type"` + UDP clientConfigTransportUDP `mapstructure:"udp"` +} + +type clientConfigObfsSalamander struct { + Password string `mapstructure:"password"` +} + +type clientConfigObfs struct { + Type string `mapstructure:"type"` + Salamander clientConfigObfsSalamander `mapstructure:"salamander"` +} + +type clientConfigTLS struct { + SNI string `mapstructure:"sni"` + Insecure bool `mapstructure:"insecure"` + PinSHA256 string `mapstructure:"pinSHA256"` + CA string `mapstructure:"ca"` +} + +type clientConfigQUIC struct { + InitStreamReceiveWindow uint64 `mapstructure:"initStreamReceiveWindow"` + MaxStreamReceiveWindow uint64 `mapstructure:"maxStreamReceiveWindow"` + InitConnectionReceiveWindow uint64 `mapstructure:"initConnReceiveWindow"` + MaxConnectionReceiveWindow uint64 `mapstructure:"maxConnReceiveWindow"` + MaxIdleTimeout time.Duration `mapstructure:"maxIdleTimeout"` + KeepAlivePeriod time.Duration `mapstructure:"keepAlivePeriod"` + DisablePathMTUDiscovery bool `mapstructure:"disablePathMTUDiscovery"` + Sockopts clientConfigQUICSockopts `mapstructure:"sockopts"` +} + +type clientConfigQUICSockopts struct { + BindInterface *string `mapstructure:"bindInterface"` + FirewallMark *uint32 `mapstructure:"fwmark"` + FdControlUnixSocket *string `mapstructure:"fdControlUnixSocket"` +} + +type clientConfigBandwidth struct { + Up string `mapstructure:"up"` + Down string `mapstructure:"down"` +} + +type socks5Config struct { + Listen string `mapstructure:"listen"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + DisableUDP bool `mapstructure:"disableUDP"` +} + +type httpConfig struct { + Listen string `mapstructure:"listen"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + Realm string `mapstructure:"realm"` +} + +type tcpForwardingEntry struct { + Listen string `mapstructure:"listen"` + Remote string `mapstructure:"remote"` +} + +type udpForwardingEntry struct { + Listen string `mapstructure:"listen"` + Remote string `mapstructure:"remote"` + Timeout time.Duration `mapstructure:"timeout"` +} + +type tcpTProxyConfig struct { + Listen string `mapstructure:"listen"` +} + +type udpTProxyConfig struct { + Listen string `mapstructure:"listen"` + Timeout time.Duration `mapstructure:"timeout"` +} + +type tcpRedirectConfig struct { + Listen string `mapstructure:"listen"` +} + +type tunConfig struct { + Name string `mapstructure:"name"` + MTU uint32 `mapstructure:"mtu"` + Timeout time.Duration `mapstructure:"timeout"` + Address struct { + IPv4 string `mapstructure:"ipv4"` + IPv6 string `mapstructure:"ipv6"` + } `mapstructure:"address"` + Route *struct { + Strict bool `mapstructure:"strict"` + IPv4 []string `mapstructure:"ipv4"` + IPv6 []string `mapstructure:"ipv6"` + IPv4Exclude []string `mapstructure:"ipv4Exclude"` + IPv6Exclude []string `mapstructure:"ipv6Exclude"` + } `mapstructure:"route"` +} + +func (c *clientConfig) fillServerAddr(hyConfig *client.Config) error { + if c.Server == "" { + return configError{Field: "server", Err: errors.New("server address is empty")} + } + var addr net.Addr + var err error + host, port, hostPort := parseServerAddrString(c.Server) + if !isPortHoppingPort(port) { + addr, err = net.ResolveUDPAddr("udp", hostPort) + } else { + addr, err = udphop.ResolveUDPHopAddr(hostPort) + } + if err != nil { + return configError{Field: "server", Err: err} + } + hyConfig.ServerAddr = addr + // Special handling for SNI + if c.TLS.SNI == "" { + // Use server hostname as SNI + hyConfig.TLSConfig.ServerName = host + } + return nil +} + +// fillConnFactory must be called after fillServerAddr, as we have different logic +// for ConnFactory depending on whether we have a port hopping address. +func (c *clientConfig) fillConnFactory(hyConfig *client.Config) error { + so := &sockopts.SocketOptions{ + BindInterface: c.QUIC.Sockopts.BindInterface, + FirewallMark: c.QUIC.Sockopts.FirewallMark, + FdControlUnixSocket: c.QUIC.Sockopts.FdControlUnixSocket, + } + if err := so.CheckSupported(); err != nil { + var unsupportedErr *sockopts.UnsupportedError + if errors.As(err, &unsupportedErr) { + return configError{ + Field: "quic.sockopts." + unsupportedErr.Field, + Err: errors.New("unsupported on this platform"), + } + } + return configError{Field: "quic.sockopts", Err: err} + } + // Inner PacketConn + var newFunc func(addr net.Addr) (net.PacketConn, error) + switch strings.ToLower(c.Transport.Type) { + case "", "udp": + if hyConfig.ServerAddr.Network() == "udphop" { + hopAddr := hyConfig.ServerAddr.(*udphop.UDPHopAddr) + newFunc = func(addr net.Addr) (net.PacketConn, error) { + return udphop.NewUDPHopPacketConn(hopAddr, c.Transport.UDP.HopInterval, so.ListenUDP) + } + } else { + newFunc = func(addr net.Addr) (net.PacketConn, error) { + return so.ListenUDP() + } + } + default: + return configError{Field: "transport.type", Err: errors.New("unsupported transport type")} + } + // Obfuscation + var ob obfs.Obfuscator + var err error + switch strings.ToLower(c.Obfs.Type) { + case "", "plain": + // Keep it nil + case "salamander": + ob, err = obfs.NewSalamanderObfuscator([]byte(c.Obfs.Salamander.Password)) + if err != nil { + return configError{Field: "obfs.salamander.password", Err: err} + } + default: + return configError{Field: "obfs.type", Err: errors.New("unsupported obfuscation type")} + } + hyConfig.ConnFactory = &adaptiveConnFactory{ + NewFunc: newFunc, + Obfuscator: ob, + } + return nil +} + +func (c *clientConfig) fillAuth(hyConfig *client.Config) error { + hyConfig.Auth = c.Auth + return nil +} + +func (c *clientConfig) fillTLSConfig(hyConfig *client.Config) error { + if c.TLS.SNI != "" { + hyConfig.TLSConfig.ServerName = c.TLS.SNI + } + hyConfig.TLSConfig.InsecureSkipVerify = c.TLS.Insecure + if c.TLS.PinSHA256 != "" { + nHash := normalizeCertHash(c.TLS.PinSHA256) + hyConfig.TLSConfig.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error { + for _, cert := range rawCerts { + hash := sha256.Sum256(cert) + hashHex := hex.EncodeToString(hash[:]) + if hashHex == nHash { + return nil + } + } + // No match + return errors.New("no certificate matches the pinned hash") + } + } + if c.TLS.CA != "" { + ca, err := os.ReadFile(c.TLS.CA) + if err != nil { + return configError{Field: "tls.ca", Err: err} + } + cPool := x509.NewCertPool() + if !cPool.AppendCertsFromPEM(ca) { + return configError{Field: "tls.ca", Err: errors.New("failed to parse CA certificate")} + } + hyConfig.TLSConfig.RootCAs = cPool + } + return nil +} + +func (c *clientConfig) fillQUICConfig(hyConfig *client.Config) error { + hyConfig.QUICConfig = client.QUICConfig{ + InitialStreamReceiveWindow: c.QUIC.InitStreamReceiveWindow, + MaxStreamReceiveWindow: c.QUIC.MaxStreamReceiveWindow, + InitialConnectionReceiveWindow: c.QUIC.InitConnectionReceiveWindow, + MaxConnectionReceiveWindow: c.QUIC.MaxConnectionReceiveWindow, + MaxIdleTimeout: c.QUIC.MaxIdleTimeout, + KeepAlivePeriod: c.QUIC.KeepAlivePeriod, + DisablePathMTUDiscovery: c.QUIC.DisablePathMTUDiscovery, + } + return nil +} + +func (c *clientConfig) fillBandwidthConfig(hyConfig *client.Config) error { + // New core now allows users to omit bandwidth values and use built-in congestion control + var err error + if c.Bandwidth.Up != "" { + hyConfig.BandwidthConfig.MaxTx, err = utils.ConvBandwidth(c.Bandwidth.Up) + if err != nil { + return configError{Field: "bandwidth.up", Err: err} + } + } + if c.Bandwidth.Down != "" { + hyConfig.BandwidthConfig.MaxRx, err = utils.ConvBandwidth(c.Bandwidth.Down) + if err != nil { + return configError{Field: "bandwidth.down", Err: err} + } + } + return nil +} + +func (c *clientConfig) fillFastOpen(hyConfig *client.Config) error { + hyConfig.FastOpen = c.FastOpen + return nil +} + +// URI generates a URI for sharing the config with others. +// Note that only the bare minimum of information required to +// connect to the server is included in the URI, specifically: +// - server address +// - authentication +// - obfuscation type +// - obfuscation password +// - TLS SNI +// - TLS insecure +// - TLS pinned SHA256 hash (normalized) +func (c *clientConfig) URI() string { + q := url.Values{} + switch strings.ToLower(c.Obfs.Type) { + case "salamander": + q.Set("obfs", "salamander") + q.Set("obfs-password", c.Obfs.Salamander.Password) + } + if c.TLS.SNI != "" { + q.Set("sni", c.TLS.SNI) + } + if c.TLS.Insecure { + q.Set("insecure", "1") + } + if c.TLS.PinSHA256 != "" { + q.Set("pinSHA256", normalizeCertHash(c.TLS.PinSHA256)) + } + var user *url.Userinfo + if c.Auth != "" { + // We need to handle the special case of user:pass pairs + rs := strings.SplitN(c.Auth, ":", 2) + if len(rs) == 2 { + user = url.UserPassword(rs[0], rs[1]) + } else { + user = url.User(c.Auth) + } + } + u := url.URL{ + Scheme: "hysteria2", + User: user, + Host: c.Server, + Path: "/", + RawQuery: q.Encode(), + } + return u.String() +} + +// parseURI tries to parse the server address field as a URI, +// and fills the config with the information contained in the URI. +// Returns whether the server address field is a valid URI. +// This allows a user to use put a URI as the server address and +// omit the fields that are already contained in the URI. +func (c *clientConfig) parseURI() bool { + u, err := url.Parse(c.Server) + if err != nil { + return false + } + if u.Scheme != "hysteria2" && u.Scheme != "hy2" { + return false + } + if u.User != nil { + auth, err := url.QueryUnescape(u.User.String()) + if err != nil { + return false + } + c.Auth = auth + } + c.Server = u.Host + q := u.Query() + if obfsType := q.Get("obfs"); obfsType != "" { + c.Obfs.Type = obfsType + switch strings.ToLower(obfsType) { + case "salamander": + c.Obfs.Salamander.Password = q.Get("obfs-password") + } + } + if sni := q.Get("sni"); sni != "" { + c.TLS.SNI = sni + } + if insecure, err := strconv.ParseBool(q.Get("insecure")); err == nil { + c.TLS.Insecure = insecure + } + if pinSHA256 := q.Get("pinSHA256"); pinSHA256 != "" { + c.TLS.PinSHA256 = pinSHA256 + } + return true +} + +// Config validates the fields and returns a ready-to-use Hysteria client config +func (c *clientConfig) Config() (*client.Config, error) { + c.parseURI() + hyConfig := &client.Config{} + fillers := []func(*client.Config) error{ + c.fillServerAddr, + c.fillConnFactory, + c.fillAuth, + c.fillTLSConfig, + c.fillQUICConfig, + c.fillBandwidthConfig, + c.fillFastOpen, + } + for _, f := range fillers { + if err := f(hyConfig); err != nil { + return nil, err + } + } + return hyConfig, nil +} + +func runClient(cmd *cobra.Command, args []string) { + logger.Info("client mode") + + if err := viper.ReadInConfig(); err != nil { + logger.Fatal("failed to read client config", zap.Error(err)) + } + var config clientConfig + if err := viper.Unmarshal(&config); err != nil { + logger.Fatal("failed to parse client config", zap.Error(err)) + } + + c, err := client.NewReconnectableClient( + config.Config, + func(c client.Client, info *client.HandshakeInfo, count int) { + connectLog(info, count) + // On the client side, we start checking for updates after we successfully connect + // to the server, which, depending on whether lazy mode is enabled, may or may not + // be immediately after the client starts. We don't want the update check request + // to interfere with the lazy mode option. + if count == 1 && !disableUpdateCheck { + go runCheckUpdateClient(c) + } + }, config.Lazy) + if err != nil { + logger.Fatal("failed to initialize client", zap.Error(err)) + } + defer c.Close() + + uri := config.URI() + logger.Info("use this URI to share your server", zap.String("uri", uri)) + if showQR { + utils.PrintQR(uri) + } + + // Register modes + var runner clientModeRunner + if config.SOCKS5 != nil { + runner.Add("SOCKS5 server", func() error { + return clientSOCKS5(*config.SOCKS5, c) + }) + } + if config.HTTP != nil { + runner.Add("HTTP proxy server", func() error { + return clientHTTP(*config.HTTP, c) + }) + } + if len(config.TCPForwarding) > 0 { + runner.Add("TCP forwarding", func() error { + return clientTCPForwarding(config.TCPForwarding, c) + }) + } + if len(config.UDPForwarding) > 0 { + runner.Add("UDP forwarding", func() error { + return clientUDPForwarding(config.UDPForwarding, c) + }) + } + if config.TCPTProxy != nil { + runner.Add("TCP transparent proxy", func() error { + return clientTCPTProxy(*config.TCPTProxy, c) + }) + } + if config.UDPTProxy != nil { + runner.Add("UDP transparent proxy", func() error { + return clientUDPTProxy(*config.UDPTProxy, c) + }) + } + if config.TCPRedirect != nil { + runner.Add("TCP redirect", func() error { + return clientTCPRedirect(*config.TCPRedirect, c) + }) + } + if config.TUN != nil { + runner.Add("TUN", func() error { + return clientTUN(*config.TUN, c) + }) + } + + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) + defer signal.Stop(signalChan) + + runnerChan := make(chan clientModeRunnerResult, 1) + go func() { + runnerChan <- runner.Run() + }() + + select { + case <-signalChan: + logger.Info("received signal, shutting down gracefully") + case r := <-runnerChan: + if r.OK { + logger.Info(r.Msg) + } else { + _ = c.Close() // Close the client here as Fatal will exit the program without running defer + if r.Err != nil { + logger.Fatal(r.Msg, zap.Error(r.Err)) + } else { + logger.Fatal(r.Msg) + } + } + } +} + +type clientModeRunner struct { + ModeMap map[string]func() error +} + +type clientModeRunnerResult struct { + OK bool + Msg string + Err error +} + +func (r *clientModeRunner) Add(name string, f func() error) { + if r.ModeMap == nil { + r.ModeMap = make(map[string]func() error) + } + r.ModeMap[name] = f +} + +func (r *clientModeRunner) Run() clientModeRunnerResult { + if len(r.ModeMap) == 0 { + return clientModeRunnerResult{OK: false, Msg: "no mode specified"} + } + + type modeError struct { + Name string + Err error + } + errChan := make(chan modeError, len(r.ModeMap)) + for name, f := range r.ModeMap { + go func(name string, f func() error) { + err := f() + errChan <- modeError{name, err} + }(name, f) + } + // Fatal if any one of the modes fails + for i := 0; i < len(r.ModeMap); i++ { + e := <-errChan + if e.Err != nil { + return clientModeRunnerResult{OK: false, Msg: "failed to run " + e.Name, Err: e.Err} + } + } + + // We don't really have any such cases, as currently none of our modes would stop on themselves without error. + // But we leave the possibility here for future expansion. + return clientModeRunnerResult{OK: true, Msg: "finished without error"} +} + +func clientSOCKS5(config socks5Config, c client.Client) error { + if config.Listen == "" { + return configError{Field: "listen", Err: errors.New("listen address is empty")} + } + l, err := proxymux.ListenSOCKS(config.Listen) + if err != nil { + return configError{Field: "listen", Err: err} + } + var authFunc func(username, password string) bool + username, password := config.Username, config.Password + if username != "" && password != "" { + authFunc = func(u, p string) bool { + return u == username && p == password + } + } + s := socks5.Server{ + HyClient: c, + AuthFunc: authFunc, + DisableUDP: config.DisableUDP, + EventLogger: &socks5Logger{}, + } + logger.Info("SOCKS5 server listening", zap.String("addr", config.Listen)) + return s.Serve(l) +} + +func clientHTTP(config httpConfig, c client.Client) error { + if config.Listen == "" { + return configError{Field: "listen", Err: errors.New("listen address is empty")} + } + l, err := proxymux.ListenHTTP(config.Listen) + if err != nil { + return configError{Field: "listen", Err: err} + } + var authFunc func(username, password string) bool + username, password := config.Username, config.Password + if username != "" && password != "" { + authFunc = func(u, p string) bool { + return u == username && p == password + } + } + if config.Realm == "" { + config.Realm = "Hysteria" + } + h := http.Server{ + HyClient: c, + AuthFunc: authFunc, + AuthRealm: config.Realm, + EventLogger: &httpLogger{}, + } + logger.Info("HTTP proxy server listening", zap.String("addr", config.Listen)) + return h.Serve(l) +} + +func clientTCPForwarding(entries []tcpForwardingEntry, c client.Client) error { + errChan := make(chan error, len(entries)) + for _, e := range entries { + if e.Listen == "" { + return configError{Field: "listen", Err: errors.New("listen address is empty")} + } + if e.Remote == "" { + return configError{Field: "remote", Err: errors.New("remote address is empty")} + } + l, err := correctnet.Listen("tcp", e.Listen) + if err != nil { + return configError{Field: "listen", Err: err} + } + logger.Info("TCP forwarding listening", zap.String("addr", e.Listen), zap.String("remote", e.Remote)) + go func(remote string) { + t := &forwarding.TCPTunnel{ + HyClient: c, + Remote: remote, + EventLogger: &tcpLogger{}, + } + errChan <- t.Serve(l) + }(e.Remote) + } + // Return if any one of the forwarding fails + return <-errChan +} + +func clientUDPForwarding(entries []udpForwardingEntry, c client.Client) error { + errChan := make(chan error, len(entries)) + for _, e := range entries { + if e.Listen == "" { + return configError{Field: "listen", Err: errors.New("listen address is empty")} + } + if e.Remote == "" { + return configError{Field: "remote", Err: errors.New("remote address is empty")} + } + l, err := correctnet.ListenPacket("udp", e.Listen) + if err != nil { + return configError{Field: "listen", Err: err} + } + logger.Info("UDP forwarding listening", zap.String("addr", e.Listen), zap.String("remote", e.Remote)) + go func(remote string, timeout time.Duration) { + u := &forwarding.UDPTunnel{ + HyClient: c, + Remote: remote, + Timeout: timeout, + EventLogger: &udpLogger{}, + } + errChan <- u.Serve(l) + }(e.Remote, e.Timeout) + } + // Return if any one of the forwarding fails + return <-errChan +} + +func clientTCPTProxy(config tcpTProxyConfig, c client.Client) error { + if config.Listen == "" { + return configError{Field: "listen", Err: errors.New("listen address is empty")} + } + laddr, err := net.ResolveTCPAddr("tcp", config.Listen) + if err != nil { + return configError{Field: "listen", Err: err} + } + p := &tproxy.TCPTProxy{ + HyClient: c, + EventLogger: &tcpTProxyLogger{}, + } + logger.Info("TCP transparent proxy listening", zap.String("addr", config.Listen)) + return p.ListenAndServe(laddr) +} + +func clientUDPTProxy(config udpTProxyConfig, c client.Client) error { + if config.Listen == "" { + return configError{Field: "listen", Err: errors.New("listen address is empty")} + } + laddr, err := net.ResolveUDPAddr("udp", config.Listen) + if err != nil { + return configError{Field: "listen", Err: err} + } + p := &tproxy.UDPTProxy{ + HyClient: c, + Timeout: config.Timeout, + EventLogger: &udpTProxyLogger{}, + } + logger.Info("UDP transparent proxy listening", zap.String("addr", config.Listen)) + return p.ListenAndServe(laddr) +} + +func clientTCPRedirect(config tcpRedirectConfig, c client.Client) error { + if config.Listen == "" { + return configError{Field: "listen", Err: errors.New("listen address is empty")} + } + laddr, err := net.ResolveTCPAddr("tcp", config.Listen) + if err != nil { + return configError{Field: "listen", Err: err} + } + p := &redirect.TCPRedirect{ + HyClient: c, + EventLogger: &tcpRedirectLogger{}, + } + logger.Info("TCP redirect listening", zap.String("addr", config.Listen)) + return p.ListenAndServe(laddr) +} + +func clientTUN(config tunConfig, c client.Client) error { + supportedPlatforms := []string{"linux", "darwin", "windows", "android"} + if !slices.Contains(supportedPlatforms, runtime.GOOS) { + logger.Error("TUN is not supported on this platform", zap.String("platform", runtime.GOOS)) + } + if config.Name == "" { + return configError{Field: "name", Err: errors.New("name is empty")} + } + if config.MTU == 0 { + config.MTU = 1500 + } + timeout := int64(config.Timeout.Seconds()) + if timeout == 0 { + timeout = 300 + } + if config.Address.IPv4 == "" { + config.Address.IPv4 = "100.100.100.101/30" + } + prefix4, err := netip.ParsePrefix(config.Address.IPv4) + if err != nil { + return configError{Field: "address.ipv4", Err: err} + } + if config.Address.IPv6 == "" { + config.Address.IPv6 = "2001::ffff:ffff:ffff:fff1/126" + } + prefix6, err := netip.ParsePrefix(config.Address.IPv6) + if err != nil { + return configError{Field: "address.ipv6", Err: err} + } + server := &tun.Server{ + HyClient: c, + EventLogger: &tunLogger{}, + Logger: logger, + IfName: config.Name, + MTU: config.MTU, + Timeout: timeout, + Inet4Address: []netip.Prefix{prefix4}, + Inet6Address: []netip.Prefix{prefix6}, + } + if config.Route != nil { + server.AutoRoute = true + server.StructRoute = config.Route.Strict + + parsePrefixes := func(field string, ss []string) ([]netip.Prefix, error) { + var prefixes []netip.Prefix + for i, s := range ss { + var p netip.Prefix + if strings.Contains(s, "/") { + var err error + p, err = netip.ParsePrefix(s) + if err != nil { + return nil, configError{Field: fmt.Sprintf("%s[%d]", field, i), Err: err} + } + } else { + pa, err := netip.ParseAddr(s) + if err != nil { + return nil, configError{Field: fmt.Sprintf("%s[%d]", field, i), Err: err} + } + p = netip.PrefixFrom(pa, pa.BitLen()) + } + prefixes = append(prefixes, p) + } + return prefixes, nil + } + + server.Inet4RouteAddress, err = parsePrefixes("route.ipv4", config.Route.IPv4) + if err != nil { + return err + } + server.Inet6RouteAddress, err = parsePrefixes("route.ipv6", config.Route.IPv6) + if err != nil { + return err + } + server.Inet4RouteExcludeAddress, err = parsePrefixes("route.ipv4Exclude", config.Route.IPv4Exclude) + if err != nil { + return err + } + server.Inet6RouteExcludeAddress, err = parsePrefixes("route.ipv6Exclude", config.Route.IPv6Exclude) + if err != nil { + return err + } + } + logger.Info("TUN listening", zap.String("interface", config.Name)) + return server.Serve() +} + +// parseServerAddrString parses server address string. +// Server address can be in either "host:port" or "host" format (in which case we assume port 443). +func parseServerAddrString(addrStr string) (host, port, hostPort string) { + h, p, err := net.SplitHostPort(addrStr) + if err != nil { + return addrStr, "443", net.JoinHostPort(addrStr, "443") + } + return h, p, addrStr +} + +// isPortHoppingPort returns whether the port string is a port hopping port. +// We consider a port string to be a port hopping port if it contains "-" or ",". +func isPortHoppingPort(port string) bool { + return strings.Contains(port, "-") || strings.Contains(port, ",") +} + +// normalizeCertHash normalizes a certificate hash string. +// It converts all characters to lowercase and removes possible separators such as ":" and "-". +func normalizeCertHash(hash string) string { + r := strings.ToLower(hash) + r = strings.ReplaceAll(r, ":", "") + r = strings.ReplaceAll(r, "-", "") + return r +} + +type adaptiveConnFactory struct { + NewFunc func(addr net.Addr) (net.PacketConn, error) + Obfuscator obfs.Obfuscator // nil if no obfuscation +} + +func (f *adaptiveConnFactory) New(addr net.Addr) (net.PacketConn, error) { + if f.Obfuscator == nil { + return f.NewFunc(addr) + } else { + conn, err := f.NewFunc(addr) + if err != nil { + return nil, err + } + return obfs.WrapPacketConn(conn, f.Obfuscator), nil + } +} + +func connectLog(info *client.HandshakeInfo, count int) { + logger.Info("connected to server", + zap.Bool("udpEnabled", info.UDPEnabled), + zap.Uint64("tx", info.Tx), + zap.Int("count", count)) +} + +type socks5Logger struct{} + +func (l *socks5Logger) TCPRequest(addr net.Addr, reqAddr string) { + logger.Debug("SOCKS5 TCP request", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr)) +} + +func (l *socks5Logger) TCPError(addr net.Addr, reqAddr string, err error) { + if err == nil { + logger.Debug("SOCKS5 TCP closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr)) + } else { + logger.Warn("SOCKS5 TCP error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr), zap.Error(err)) + } +} + +func (l *socks5Logger) UDPRequest(addr net.Addr) { + logger.Debug("SOCKS5 UDP request", zap.String("addr", addr.String())) +} + +func (l *socks5Logger) UDPError(addr net.Addr, err error) { + if err == nil { + logger.Debug("SOCKS5 UDP closed", zap.String("addr", addr.String())) + } else { + logger.Warn("SOCKS5 UDP error", zap.String("addr", addr.String()), zap.Error(err)) + } +} + +type httpLogger struct{} + +func (l *httpLogger) ConnectRequest(addr net.Addr, reqAddr string) { + logger.Debug("HTTP CONNECT request", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr)) +} + +func (l *httpLogger) ConnectError(addr net.Addr, reqAddr string, err error) { + if err == nil { + logger.Debug("HTTP CONNECT closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr)) + } else { + logger.Warn("HTTP CONNECT error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr), zap.Error(err)) + } +} + +func (l *httpLogger) HTTPRequest(addr net.Addr, reqURL string) { + logger.Debug("HTTP request", zap.String("addr", addr.String()), zap.String("reqURL", reqURL)) +} + +func (l *httpLogger) HTTPError(addr net.Addr, reqURL string, err error) { + if err == nil { + logger.Debug("HTTP closed", zap.String("addr", addr.String()), zap.String("reqURL", reqURL)) + } else { + logger.Warn("HTTP error", zap.String("addr", addr.String()), zap.String("reqURL", reqURL), zap.Error(err)) + } +} + +type tcpLogger struct{} + +func (l *tcpLogger) Connect(addr net.Addr) { + logger.Debug("TCP forwarding connect", zap.String("addr", addr.String())) +} + +func (l *tcpLogger) Error(addr net.Addr, err error) { + if err == nil { + logger.Debug("TCP forwarding closed", zap.String("addr", addr.String())) + } else { + logger.Warn("TCP forwarding error", zap.String("addr", addr.String()), zap.Error(err)) + } +} + +type udpLogger struct{} + +func (l *udpLogger) Connect(addr net.Addr) { + logger.Debug("UDP forwarding connect", zap.String("addr", addr.String())) +} + +func (l *udpLogger) Error(addr net.Addr, err error) { + if err == nil { + logger.Debug("UDP forwarding closed", zap.String("addr", addr.String())) + } else { + logger.Warn("UDP forwarding error", zap.String("addr", addr.String()), zap.Error(err)) + } +} + +type tcpTProxyLogger struct{} + +func (l *tcpTProxyLogger) Connect(addr, reqAddr net.Addr) { + logger.Debug("TCP transparent proxy connect", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String())) +} + +func (l *tcpTProxyLogger) Error(addr, reqAddr net.Addr, err error) { + if err == nil { + logger.Debug("TCP transparent proxy closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String())) + } else { + logger.Warn("TCP transparent proxy error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()), zap.Error(err)) + } +} + +type udpTProxyLogger struct{} + +func (l *udpTProxyLogger) Connect(addr, reqAddr net.Addr) { + logger.Debug("UDP transparent proxy connect", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String())) +} + +func (l *udpTProxyLogger) Error(addr, reqAddr net.Addr, err error) { + if err == nil { + logger.Debug("UDP transparent proxy closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String())) + } else { + logger.Warn("UDP transparent proxy error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()), zap.Error(err)) + } +} + +type tcpRedirectLogger struct{} + +func (l *tcpRedirectLogger) Connect(addr, reqAddr net.Addr) { + logger.Debug("TCP redirect connect", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String())) +} + +func (l *tcpRedirectLogger) Error(addr, reqAddr net.Addr, err error) { + if err == nil { + logger.Debug("TCP redirect closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String())) + } else { + logger.Warn("TCP redirect error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()), zap.Error(err)) + } +} + +type tunLogger struct{} + +func (l *tunLogger) TCPRequest(addr, reqAddr string) { + logger.Debug("TUN TCP request", zap.String("addr", addr), zap.String("reqAddr", reqAddr)) +} + +func (l *tunLogger) TCPError(addr, reqAddr string, err error) { + if err == nil { + logger.Debug("TUN TCP closed", zap.String("addr", addr), zap.String("reqAddr", reqAddr)) + } else { + logger.Warn("TUN TCP error", zap.String("addr", addr), zap.String("reqAddr", reqAddr), zap.Error(err)) + } +} + +func (l *tunLogger) UDPRequest(addr string) { + logger.Debug("TUN UDP request", zap.String("addr", addr)) +} + +func (l *tunLogger) UDPError(addr string, err error) { + if err == nil { + logger.Debug("TUN UDP closed", zap.String("addr", addr)) + } else { + logger.Warn("TUN UDP error", zap.String("addr", addr), zap.Error(err)) + } +} diff --git a/v2rayng/hysteria/app/cmd/client_test.go b/v2rayng/hysteria/app/cmd/client_test.go new file mode 100644 index 0000000000..10b2d9911e --- /dev/null +++ b/v2rayng/hysteria/app/cmd/client_test.go @@ -0,0 +1,204 @@ +package cmd + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/spf13/viper" +) + +// TestClientConfig tests the parsing of the client config +func TestClientConfig(t *testing.T) { + viper.SetConfigFile("client_test.yaml") + err := viper.ReadInConfig() + assert.NoError(t, err) + var config clientConfig + err = viper.Unmarshal(&config) + assert.NoError(t, err) + assert.Equal(t, config, clientConfig{ + Server: "example.com", + Auth: "weak_ahh_password", + Transport: clientConfigTransport{ + Type: "udp", + UDP: clientConfigTransportUDP{ + HopInterval: 30 * time.Second, + }, + }, + Obfs: clientConfigObfs{ + Type: "salamander", + Salamander: clientConfigObfsSalamander{ + Password: "cry_me_a_r1ver", + }, + }, + TLS: clientConfigTLS{ + SNI: "another.example.com", + Insecure: true, + PinSHA256: "114515DEADBEEF", + CA: "custom_ca.crt", + }, + QUIC: clientConfigQUIC{ + InitStreamReceiveWindow: 1145141, + MaxStreamReceiveWindow: 1145142, + InitConnectionReceiveWindow: 1145143, + MaxConnectionReceiveWindow: 1145144, + MaxIdleTimeout: 10 * time.Second, + KeepAlivePeriod: 4 * time.Second, + DisablePathMTUDiscovery: true, + Sockopts: clientConfigQUICSockopts{ + BindInterface: stringRef("eth0"), + FirewallMark: uint32Ref(1234), + FdControlUnixSocket: stringRef("test.sock"), + }, + }, + Bandwidth: clientConfigBandwidth{ + Up: "200 mbps", + Down: "1 gbps", + }, + FastOpen: true, + Lazy: true, + SOCKS5: &socks5Config{ + Listen: "127.0.0.1:1080", + Username: "anon", + Password: "bro", + DisableUDP: true, + }, + HTTP: &httpConfig{ + Listen: "127.0.0.1:8080", + Username: "qqq", + Password: "bruh", + Realm: "martian", + }, + TCPForwarding: []tcpForwardingEntry{ + { + Listen: "127.0.0.1:8088", + Remote: "internal.example.com:80", + }, + }, + UDPForwarding: []udpForwardingEntry{ + { + Listen: "127.0.0.1:5353", + Remote: "internal.example.com:53", + Timeout: 50 * time.Second, + }, + }, + TCPTProxy: &tcpTProxyConfig{ + Listen: "127.0.0.1:2500", + }, + UDPTProxy: &udpTProxyConfig{ + Listen: "127.0.0.1:2501", + Timeout: 20 * time.Second, + }, + TCPRedirect: &tcpRedirectConfig{ + Listen: "127.0.0.1:3500", + }, + TUN: &tunConfig{ + Name: "hytun", + MTU: 1500, + Timeout: 60 * time.Second, + Address: struct { + IPv4 string `mapstructure:"ipv4"` + IPv6 string `mapstructure:"ipv6"` + }{IPv4: "100.100.100.101/30", IPv6: "2001::ffff:ffff:ffff:fff1/126"}, + Route: &struct { + Strict bool `mapstructure:"strict"` + IPv4 []string `mapstructure:"ipv4"` + IPv6 []string `mapstructure:"ipv6"` + IPv4Exclude []string `mapstructure:"ipv4Exclude"` + IPv6Exclude []string `mapstructure:"ipv6Exclude"` + }{ + Strict: true, + IPv4: []string{"0.0.0.0/0"}, + IPv6: []string{"2000::/3"}, + IPv4Exclude: []string{"192.0.2.1/32"}, + IPv6Exclude: []string{"2001:db8::1/128"}, + }, + }, + }) +} + +// TestClientConfigURI tests URI-related functions of clientConfig +func TestClientConfigURI(t *testing.T) { + tests := []struct { + uri string + uriOK bool + config *clientConfig + }{ + { + uri: "hysteria2://god@zilla.jp/", + uriOK: true, + config: &clientConfig{ + Server: "zilla.jp", + Auth: "god", + }, + }, + { + uri: "hysteria2://john:wick@continental.org:4443/", + uriOK: true, + config: &clientConfig{ + Server: "continental.org:4443", + Auth: "john:wick", + }, + }, + { + uri: "hysteria2://saul@better.call:7000-10000,20000/", + uriOK: true, + config: &clientConfig{ + Server: "better.call:7000-10000,20000", + Auth: "saul", + }, + }, + { + uri: "hysteria2://noauth.com/?insecure=1&obfs=salamander&obfs-password=66ccff&pinSHA256=deadbeef&sni=crap.cc", + uriOK: true, + config: &clientConfig{ + Server: "noauth.com", + Auth: "", + Obfs: clientConfigObfs{ + Type: "salamander", + Salamander: clientConfigObfsSalamander{ + Password: "66ccff", + }, + }, + TLS: clientConfigTLS{ + SNI: "crap.cc", + Insecure: true, + PinSHA256: "deadbeef", + }, + }, + }, + { + uri: "invalid.bs", + uriOK: false, + config: nil, + }, + { + uri: "https://www.google.com/search?q=test", + uriOK: false, + config: nil, + }, + } + for _, test := range tests { + t.Run(test.uri, func(t *testing.T) { + // Test parseURI + nc := &clientConfig{Server: test.uri} + assert.Equal(t, nc.parseURI(), test.uriOK) + if test.uriOK { + assert.Equal(t, nc, test.config) + } + // Test URI generation + if test.config != nil { + assert.Equal(t, test.config.URI(), test.uri) + } + }) + } +} + +func stringRef(s string) *string { + return &s +} + +func uint32Ref(i uint32) *uint32 { + return &i +} diff --git a/v2rayng/hysteria/app/cmd/client_test.yaml b/v2rayng/hysteria/app/cmd/client_test.yaml new file mode 100644 index 0000000000..e8438f6c4c --- /dev/null +++ b/v2rayng/hysteria/app/cmd/client_test.yaml @@ -0,0 +1,85 @@ +server: example.com + +auth: weak_ahh_password + +transport: + type: udp + udp: + hopInterval: 30s + +obfs: + type: salamander + salamander: + password: cry_me_a_r1ver + +tls: + sni: another.example.com + insecure: true + pinSHA256: 114515DEADBEEF + ca: custom_ca.crt + +quic: + initStreamReceiveWindow: 1145141 + maxStreamReceiveWindow: 1145142 + initConnReceiveWindow: 1145143 + maxConnReceiveWindow: 1145144 + maxIdleTimeout: 10s + keepAlivePeriod: 4s + disablePathMTUDiscovery: true + sockopts: + bindInterface: eth0 + fwmark: 1234 + fdControlUnixSocket: test.sock + +bandwidth: + up: 200 mbps + down: 1 gbps + +fastOpen: true + +lazy: true + +socks5: + listen: 127.0.0.1:1080 + username: anon + password: bro + disableUDP: true + +http: + listen: 127.0.0.1:8080 + username: qqq + password: bruh + realm: martian + +tcpForwarding: + - listen: 127.0.0.1:8088 + remote: internal.example.com:80 + +udpForwarding: + - listen: 127.0.0.1:5353 + remote: internal.example.com:53 + timeout: 50s + +tcpTProxy: + listen: 127.0.0.1:2500 + +udpTProxy: + listen: 127.0.0.1:2501 + timeout: 20s + +tcpRedirect: + listen: 127.0.0.1:3500 + +tun: + name: "hytun" + mtu: 1500 + timeout: 1m + address: + ipv4: 100.100.100.101/30 + ipv6: 2001::ffff:ffff:ffff:fff1/126 + route: + strict: true + ipv4: [ 0.0.0.0/0 ] + ipv6: [ "2000::/3" ] + ipv4Exclude: [ 192.0.2.1/32 ] + ipv6Exclude: [ "2001:db8::1/128" ] diff --git a/v2rayng/hysteria/app/cmd/errors.go b/v2rayng/hysteria/app/cmd/errors.go new file mode 100644 index 0000000000..3d0234aa2d --- /dev/null +++ b/v2rayng/hysteria/app/cmd/errors.go @@ -0,0 +1,18 @@ +package cmd + +import ( + "fmt" +) + +type configError struct { + Field string + Err error +} + +func (e configError) Error() string { + return fmt.Sprintf("invalid config: %s: %s", e.Field, e.Err) +} + +func (e configError) Unwrap() error { + return e.Err +} diff --git a/v2rayng/hysteria/app/cmd/ping.go b/v2rayng/hysteria/app/cmd/ping.go new file mode 100644 index 0000000000..db450525df --- /dev/null +++ b/v2rayng/hysteria/app/cmd/ping.go @@ -0,0 +1,63 @@ +package cmd + +import ( + "time" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/zap" + + "github.com/apernet/hysteria/core/v2/client" +) + +// pingCmd represents the ping command +var pingCmd = &cobra.Command{ + Use: "ping address", + Short: "Ping mode", + Long: "Perform a TCP ping to a specified remote address through the proxy server. Can be used as a simple connectivity test.", + Run: runPing, +} + +func init() { + rootCmd.AddCommand(pingCmd) +} + +func runPing(cmd *cobra.Command, args []string) { + logger.Info("ping mode") + + if len(args) != 1 { + logger.Fatal("must specify one and only one address") + } + addr := args[0] + + if err := viper.ReadInConfig(); err != nil { + logger.Fatal("failed to read client config", zap.Error(err)) + } + var config clientConfig + if err := viper.Unmarshal(&config); err != nil { + logger.Fatal("failed to parse client config", zap.Error(err)) + } + hyConfig, err := config.Config() + if err != nil { + logger.Fatal("failed to load client config", zap.Error(err)) + } + + c, info, err := client.NewClient(hyConfig) + if err != nil { + logger.Fatal("failed to initialize client", zap.Error(err)) + } + defer c.Close() + logger.Info("connected to server", + zap.Bool("udpEnabled", info.UDPEnabled), + zap.Uint64("tx", info.Tx)) + + logger.Info("connecting", zap.String("addr", addr)) + start := time.Now() + conn, err := c.TCP(addr) + if err != nil { + logger.Fatal("failed to connect", zap.Error(err), zap.String("time", time.Since(start).String())) + } + defer conn.Close() + + logger.Info("connected", zap.String("time", time.Since(start).String())) +} diff --git a/v2rayng/hysteria/app/cmd/root.go b/v2rayng/hysteria/app/cmd/root.go new file mode 100644 index 0000000000..a58e2bc21d --- /dev/null +++ b/v2rayng/hysteria/app/cmd/root.go @@ -0,0 +1,176 @@ +package cmd + +import ( + "fmt" + "os" + "strconv" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +const ( + appLogo = ` +░█░█░█░█░█▀▀░▀█▀░█▀▀░█▀▄░▀█▀░█▀█░░░▀▀▄ +░█▀█░░█░░▀▀█░░█░░█▀▀░█▀▄░░█░░█▀█░░░▄▀░ +░▀░▀░░▀░░▀▀▀░░▀░░▀▀▀░▀░▀░▀▀▀░▀░▀░░░▀▀▀ +` + appDesc = "a powerful, lightning fast and censorship resistant proxy" + appAuthors = "Aperture Internet Laboratory " + + appLogLevelEnv = "HYSTERIA_LOG_LEVEL" + appLogFormatEnv = "HYSTERIA_LOG_FORMAT" + appDisableUpdateCheckEnv = "HYSTERIA_DISABLE_UPDATE_CHECK" + appACMEDirEnv = "HYSTERIA_ACME_DIR" +) + +var ( + // These values will be injected by the build system + appVersion = "Unknown" + appDate = "Unknown" + appType = "Unknown" // aka channel + appToolchain = "Unknown" + appCommit = "Unknown" + appPlatform = "Unknown" + appArch = "Unknown" + libVersion = "Unknown" + + appVersionLong = fmt.Sprintf("Version:\t%s\n"+ + "BuildDate:\t%s\n"+ + "BuildType:\t%s\n"+ + "Toolchain:\t%s\n"+ + "CommitHash:\t%s\n"+ + "Platform:\t%s\n"+ + "Architecture:\t%s\n"+ + "LibVersion:\t%s", + appVersion, appDate, appType, appToolchain, appCommit, appPlatform, appArch, libVersion) + + appAboutLong = fmt.Sprintf("%s\n%s\n%s\n\n%s", appLogo, appDesc, appAuthors, appVersionLong) +) + +var logger *zap.Logger + +// Flags +var ( + cfgFile string + logLevel string + logFormat string + disableUpdateCheck bool +) + +var rootCmd = &cobra.Command{ + Use: "hysteria", + Short: appDesc, + Long: appAboutLong, + Run: runClient, // Default to client mode +} + +var logLevelMap = map[string]zapcore.Level{ + "debug": zapcore.DebugLevel, + "info": zapcore.InfoLevel, + "warn": zapcore.WarnLevel, + "error": zapcore.ErrorLevel, +} + +var logFormatMap = map[string]zapcore.EncoderConfig{ + "console": { + TimeKey: "time", + LevelKey: "level", + NameKey: "logger", + MessageKey: "msg", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.CapitalColorLevelEncoder, + EncodeTime: zapcore.RFC3339TimeEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + }, + "json": { + TimeKey: "time", + LevelKey: "level", + NameKey: "logger", + MessageKey: "msg", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.LowercaseLevelEncoder, + EncodeTime: zapcore.EpochMillisTimeEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + }, +} + +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + initFlags() + cobra.MousetrapHelpText = "" // Disable the mousetrap so Windows users can run the exe directly by double-clicking + cobra.OnInitialize(initConfig) + cobra.OnInitialize(initLogger) // initLogger must come after initConfig as it depends on config +} + +func initFlags() { + rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file") + rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", envOrDefaultString(appLogLevelEnv, "info"), "log level") + rootCmd.PersistentFlags().StringVarP(&logFormat, "log-format", "f", envOrDefaultString(appLogFormatEnv, "console"), "log format") + rootCmd.PersistentFlags().BoolVar(&disableUpdateCheck, "disable-update-check", envOrDefaultBool(appDisableUpdateCheckEnv, false), "disable update check") +} + +func initConfig() { + if cfgFile != "" { + viper.SetConfigFile(cfgFile) + } else { + viper.SetConfigName("config") + viper.SetConfigType("yaml") + viper.SupportedExts = append([]string{"yaml", "yml"}, viper.SupportedExts...) + viper.AddConfigPath(".") + viper.AddConfigPath("$HOME/.hysteria") + viper.AddConfigPath("/etc/hysteria/") + } +} + +func initLogger() { + level, ok := logLevelMap[strings.ToLower(logLevel)] + if !ok { + fmt.Printf("unsupported log level: %s\n", logLevel) + os.Exit(1) + } + enc, ok := logFormatMap[strings.ToLower(logFormat)] + if !ok { + fmt.Printf("unsupported log format: %s\n", logFormat) + os.Exit(1) + } + c := zap.Config{ + Level: zap.NewAtomicLevelAt(level), + DisableCaller: true, + DisableStacktrace: true, + Encoding: strings.ToLower(logFormat), + EncoderConfig: enc, + OutputPaths: []string{"stderr"}, + ErrorOutputPaths: []string{"stderr"}, + } + var err error + logger, err = c.Build() + if err != nil { + fmt.Printf("failed to initialize logger: %s\n", err) + os.Exit(1) + } +} + +func envOrDefaultString(key, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + return def +} + +func envOrDefaultBool(key string, def bool) bool { + if v := os.Getenv(key); v != "" { + b, _ := strconv.ParseBool(v) + return b + } + return def +} diff --git a/v2rayng/hysteria/app/cmd/server.go b/v2rayng/hysteria/app/cmd/server.go new file mode 100644 index 0000000000..a4b8470940 --- /dev/null +++ b/v2rayng/hysteria/app/cmd/server.go @@ -0,0 +1,1027 @@ +package cmd + +import ( + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "net" + "net/http" + "net/http/httputil" + "net/url" + "os" + "strconv" + "strings" + "time" + + "github.com/caddyserver/certmagic" + "github.com/libdns/cloudflare" + "github.com/libdns/duckdns" + "github.com/libdns/gandi" + "github.com/libdns/godaddy" + "github.com/libdns/namedotcom" + "github.com/libdns/vultr" + "github.com/mholt/acmez/acme" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/zap" + + "github.com/apernet/hysteria/app/v2/internal/utils" + "github.com/apernet/hysteria/core/v2/server" + "github.com/apernet/hysteria/extras/v2/auth" + "github.com/apernet/hysteria/extras/v2/correctnet" + "github.com/apernet/hysteria/extras/v2/masq" + "github.com/apernet/hysteria/extras/v2/obfs" + "github.com/apernet/hysteria/extras/v2/outbounds" + "github.com/apernet/hysteria/extras/v2/sniff" + "github.com/apernet/hysteria/extras/v2/trafficlogger" + eUtils "github.com/apernet/hysteria/extras/v2/utils" +) + +const ( + defaultListenAddr = ":443" +) + +var serverCmd = &cobra.Command{ + Use: "server", + Short: "Server mode", + Run: runServer, +} + +func init() { + rootCmd.AddCommand(serverCmd) +} + +type serverConfig struct { + Listen string `mapstructure:"listen"` + Obfs serverConfigObfs `mapstructure:"obfs"` + TLS *serverConfigTLS `mapstructure:"tls"` + ACME *serverConfigACME `mapstructure:"acme"` + QUIC serverConfigQUIC `mapstructure:"quic"` + Bandwidth serverConfigBandwidth `mapstructure:"bandwidth"` + IgnoreClientBandwidth bool `mapstructure:"ignoreClientBandwidth"` + SpeedTest bool `mapstructure:"speedTest"` + DisableUDP bool `mapstructure:"disableUDP"` + UDPIdleTimeout time.Duration `mapstructure:"udpIdleTimeout"` + Auth serverConfigAuth `mapstructure:"auth"` + Resolver serverConfigResolver `mapstructure:"resolver"` + Sniff serverConfigSniff `mapstructure:"sniff"` + ACL serverConfigACL `mapstructure:"acl"` + Outbounds []serverConfigOutboundEntry `mapstructure:"outbounds"` + TrafficStats serverConfigTrafficStats `mapstructure:"trafficStats"` + Masquerade serverConfigMasquerade `mapstructure:"masquerade"` +} + +type serverConfigObfsSalamander struct { + Password string `mapstructure:"password"` +} + +type serverConfigObfs struct { + Type string `mapstructure:"type"` + Salamander serverConfigObfsSalamander `mapstructure:"salamander"` +} + +type serverConfigTLS struct { + Cert string `mapstructure:"cert"` + Key string `mapstructure:"key"` + SNIGuard string `mapstructure:"sniGuard"` // "disable", "dns-san", "strict" +} + +type serverConfigACME struct { + // Common fields + Domains []string `mapstructure:"domains"` + Email string `mapstructure:"email"` + CA string `mapstructure:"ca"` + ListenHost string `mapstructure:"listenHost"` + Dir string `mapstructure:"dir"` + + // Type selection + Type string `mapstructure:"type"` + HTTP serverConfigACMEHTTP `mapstructure:"http"` + TLS serverConfigACMETLS `mapstructure:"tls"` + DNS serverConfigACMEDNS `mapstructure:"dns"` + + // Legacy fields for backwards compatibility + // Only applicable when Type is empty + DisableHTTP bool `mapstructure:"disableHTTP"` + DisableTLSALPN bool `mapstructure:"disableTLSALPN"` + AltHTTPPort int `mapstructure:"altHTTPPort"` + AltTLSALPNPort int `mapstructure:"altTLSALPNPort"` +} + +type serverConfigACMEHTTP struct { + AltPort int `mapstructure:"altPort"` +} + +type serverConfigACMETLS struct { + AltPort int `mapstructure:"altPort"` +} + +type serverConfigACMEDNS struct { + Name string `mapstructure:"name"` + Config map[string]string `mapstructure:"config"` +} + +type serverConfigQUIC struct { + InitStreamReceiveWindow uint64 `mapstructure:"initStreamReceiveWindow"` + MaxStreamReceiveWindow uint64 `mapstructure:"maxStreamReceiveWindow"` + InitConnectionReceiveWindow uint64 `mapstructure:"initConnReceiveWindow"` + MaxConnectionReceiveWindow uint64 `mapstructure:"maxConnReceiveWindow"` + MaxIdleTimeout time.Duration `mapstructure:"maxIdleTimeout"` + MaxIncomingStreams int64 `mapstructure:"maxIncomingStreams"` + DisablePathMTUDiscovery bool `mapstructure:"disablePathMTUDiscovery"` +} + +type serverConfigBandwidth struct { + Up string `mapstructure:"up"` + Down string `mapstructure:"down"` +} + +type serverConfigAuthHTTP struct { + URL string `mapstructure:"url"` + Insecure bool `mapstructure:"insecure"` +} + +type serverConfigAuth struct { + Type string `mapstructure:"type"` + Password string `mapstructure:"password"` + UserPass map[string]string `mapstructure:"userpass"` + HTTP serverConfigAuthHTTP `mapstructure:"http"` + Command string `mapstructure:"command"` +} + +type serverConfigResolverTCP struct { + Addr string `mapstructure:"addr"` + Timeout time.Duration `mapstructure:"timeout"` +} + +type serverConfigResolverUDP struct { + Addr string `mapstructure:"addr"` + Timeout time.Duration `mapstructure:"timeout"` +} + +type serverConfigResolverTLS struct { + Addr string `mapstructure:"addr"` + Timeout time.Duration `mapstructure:"timeout"` + SNI string `mapstructure:"sni"` + Insecure bool `mapstructure:"insecure"` +} + +type serverConfigResolverHTTPS struct { + Addr string `mapstructure:"addr"` + Timeout time.Duration `mapstructure:"timeout"` + SNI string `mapstructure:"sni"` + Insecure bool `mapstructure:"insecure"` +} + +type serverConfigResolver struct { + Type string `mapstructure:"type"` + TCP serverConfigResolverTCP `mapstructure:"tcp"` + UDP serverConfigResolverUDP `mapstructure:"udp"` + TLS serverConfigResolverTLS `mapstructure:"tls"` + HTTPS serverConfigResolverHTTPS `mapstructure:"https"` +} + +type serverConfigSniff struct { + Enable bool `mapstructure:"enable"` + Timeout time.Duration `mapstructure:"timeout"` + RewriteDomain bool `mapstructure:"rewriteDomain"` + TCPPorts string `mapstructure:"tcpPorts"` + UDPPorts string `mapstructure:"udpPorts"` +} + +type serverConfigACL struct { + File string `mapstructure:"file"` + Inline []string `mapstructure:"inline"` + GeoIP string `mapstructure:"geoip"` + GeoSite string `mapstructure:"geosite"` + GeoUpdateInterval time.Duration `mapstructure:"geoUpdateInterval"` +} + +type serverConfigOutboundDirect struct { + Mode string `mapstructure:"mode"` + BindIPv4 string `mapstructure:"bindIPv4"` + BindIPv6 string `mapstructure:"bindIPv6"` + BindDevice string `mapstructure:"bindDevice"` +} + +type serverConfigOutboundSOCKS5 struct { + Addr string `mapstructure:"addr"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` +} + +type serverConfigOutboundHTTP struct { + URL string `mapstructure:"url"` + Insecure bool `mapstructure:"insecure"` +} + +type serverConfigOutboundEntry struct { + Name string `mapstructure:"name"` + Type string `mapstructure:"type"` + Direct serverConfigOutboundDirect `mapstructure:"direct"` + SOCKS5 serverConfigOutboundSOCKS5 `mapstructure:"socks5"` + HTTP serverConfigOutboundHTTP `mapstructure:"http"` +} + +type serverConfigTrafficStats struct { + Listen string `mapstructure:"listen"` + Secret string `mapstructure:"secret"` +} + +type serverConfigMasqueradeFile struct { + Dir string `mapstructure:"dir"` +} + +type serverConfigMasqueradeProxy struct { + URL string `mapstructure:"url"` + RewriteHost bool `mapstructure:"rewriteHost"` +} + +type serverConfigMasqueradeString struct { + Content string `mapstructure:"content"` + Headers map[string]string `mapstructure:"headers"` + StatusCode int `mapstructure:"statusCode"` +} + +type serverConfigMasquerade struct { + Type string `mapstructure:"type"` + File serverConfigMasqueradeFile `mapstructure:"file"` + Proxy serverConfigMasqueradeProxy `mapstructure:"proxy"` + String serverConfigMasqueradeString `mapstructure:"string"` + ListenHTTP string `mapstructure:"listenHTTP"` + ListenHTTPS string `mapstructure:"listenHTTPS"` + ForceHTTPS bool `mapstructure:"forceHTTPS"` +} + +func (c *serverConfig) fillConn(hyConfig *server.Config) error { + listenAddr := c.Listen + if listenAddr == "" { + listenAddr = defaultListenAddr + } + uAddr, err := net.ResolveUDPAddr("udp", listenAddr) + if err != nil { + return configError{Field: "listen", Err: err} + } + conn, err := correctnet.ListenUDP("udp", uAddr) + if err != nil { + return configError{Field: "listen", Err: err} + } + switch strings.ToLower(c.Obfs.Type) { + case "", "plain": + hyConfig.Conn = conn + return nil + case "salamander": + ob, err := obfs.NewSalamanderObfuscator([]byte(c.Obfs.Salamander.Password)) + if err != nil { + return configError{Field: "obfs.salamander.password", Err: err} + } + hyConfig.Conn = obfs.WrapPacketConn(conn, ob) + return nil + default: + return configError{Field: "obfs.type", Err: errors.New("unsupported obfuscation type")} + } +} + +func (c *serverConfig) fillTLSConfig(hyConfig *server.Config) error { + if c.TLS == nil && c.ACME == nil { + return configError{Field: "tls", Err: errors.New("must set either tls or acme")} + } + if c.TLS != nil && c.ACME != nil { + return configError{Field: "tls", Err: errors.New("cannot set both tls and acme")} + } + if c.TLS != nil { + // SNI guard + var sniGuard utils.SNIGuardFunc + switch strings.ToLower(c.TLS.SNIGuard) { + case "", "dns-san": + sniGuard = utils.SNIGuardDNSSAN + case "strict": + sniGuard = utils.SNIGuardStrict + case "disable": + sniGuard = nil + default: + return configError{Field: "tls.sniGuard", Err: errors.New("unsupported SNI guard")} + } + // Local TLS cert + if c.TLS.Cert == "" || c.TLS.Key == "" { + return configError{Field: "tls", Err: errors.New("empty cert or key path")} + } + certLoader := &utils.LocalCertificateLoader{ + CertFile: c.TLS.Cert, + KeyFile: c.TLS.Key, + SNIGuard: sniGuard, + } + // Try loading the cert-key pair here to catch errors early + // (e.g. invalid files or insufficient permissions) + err := certLoader.InitializeCache() + if err != nil { + var pathErr *os.PathError + if errors.As(err, &pathErr) { + if pathErr.Path == c.TLS.Cert { + return configError{Field: "tls.cert", Err: pathErr} + } + if pathErr.Path == c.TLS.Key { + return configError{Field: "tls.key", Err: pathErr} + } + } + return configError{Field: "tls", Err: err} + } + // Use GetCertificate instead of Certificates so that + // users can update the cert without restarting the server. + hyConfig.TLSConfig.GetCertificate = certLoader.GetCertificate + } else { + // ACME + dataDir := c.ACME.Dir + if dataDir == "" { + // If not specified in the config, check the environment variable + // before resorting to the default "acme" value. The main reason + // we have this is so that our setup script can set it to the + // user's home directory. + dataDir = envOrDefaultString(appACMEDirEnv, "acme") + } + cmCfg := &certmagic.Config{ + RenewalWindowRatio: certmagic.DefaultRenewalWindowRatio, + KeySource: certmagic.DefaultKeyGenerator, + Storage: &certmagic.FileStorage{Path: dataDir}, + Logger: logger, + } + cmIssuer := certmagic.NewACMEIssuer(cmCfg, certmagic.ACMEIssuer{ + Email: c.ACME.Email, + Agreed: true, + ListenHost: c.ACME.ListenHost, + Logger: logger, + }) + switch strings.ToLower(c.ACME.CA) { + case "letsencrypt", "le", "": + // Default to Let's Encrypt + cmIssuer.CA = certmagic.LetsEncryptProductionCA + case "zerossl", "zero": + cmIssuer.CA = certmagic.ZeroSSLProductionCA + eab, err := genZeroSSLEAB(c.ACME.Email) + if err != nil { + return configError{Field: "acme.ca", Err: err} + } + cmIssuer.ExternalAccount = eab + default: + return configError{Field: "acme.ca", Err: errors.New("unsupported CA")} + } + + switch strings.ToLower(c.ACME.Type) { + case "http": + cmIssuer.DisableHTTPChallenge = false + cmIssuer.DisableTLSALPNChallenge = true + cmIssuer.DNS01Solver = nil + cmIssuer.AltHTTPPort = c.ACME.HTTP.AltPort + case "tls": + cmIssuer.DisableHTTPChallenge = true + cmIssuer.DisableTLSALPNChallenge = false + cmIssuer.DNS01Solver = nil + cmIssuer.AltTLSALPNPort = c.ACME.TLS.AltPort + case "dns": + cmIssuer.DisableHTTPChallenge = true + cmIssuer.DisableTLSALPNChallenge = true + if c.ACME.DNS.Name == "" { + return configError{Field: "acme.dns.name", Err: errors.New("empty DNS provider name")} + } + if c.ACME.DNS.Config == nil { + return configError{Field: "acme.dns.config", Err: errors.New("empty DNS provider config")} + } + switch strings.ToLower(c.ACME.DNS.Name) { + case "cloudflare": + cmIssuer.DNS01Solver = &certmagic.DNS01Solver{ + DNSProvider: &cloudflare.Provider{ + APIToken: c.ACME.DNS.Config["cloudflare_api_token"], + }, + } + case "duckdns": + cmIssuer.DNS01Solver = &certmagic.DNS01Solver{ + DNSProvider: &duckdns.Provider{ + APIToken: c.ACME.DNS.Config["duckdns_api_token"], + OverrideDomain: c.ACME.DNS.Config["duckdns_override_domain"], + }, + } + case "gandi": + cmIssuer.DNS01Solver = &certmagic.DNS01Solver{ + DNSProvider: &gandi.Provider{ + BearerToken: c.ACME.DNS.Config["gandi_api_token"], + }, + } + case "godaddy": + cmIssuer.DNS01Solver = &certmagic.DNS01Solver{ + DNSProvider: &godaddy.Provider{ + APIToken: c.ACME.DNS.Config["godaddy_api_token"], + }, + } + case "namedotcom": + cmIssuer.DNS01Solver = &certmagic.DNS01Solver{ + DNSProvider: &namedotcom.Provider{ + Token: c.ACME.DNS.Config["namedotcom_token"], + User: c.ACME.DNS.Config["namedotcom_user"], + Server: c.ACME.DNS.Config["namedotcom_server"], + }, + } + case "vultr": + cmIssuer.DNS01Solver = &certmagic.DNS01Solver{ + DNSProvider: &vultr.Provider{ + APIToken: c.ACME.DNS.Config["vultr_api_token"], + }, + } + default: + return configError{Field: "acme.dns.name", Err: errors.New("unsupported DNS provider")} + } + case "": + // Legacy compatibility mode + cmIssuer.DisableHTTPChallenge = c.ACME.DisableHTTP + cmIssuer.DisableTLSALPNChallenge = c.ACME.DisableTLSALPN + cmIssuer.AltHTTPPort = c.ACME.AltHTTPPort + cmIssuer.AltTLSALPNPort = c.ACME.AltTLSALPNPort + default: + return configError{Field: "acme.type", Err: errors.New("unsupported ACME type")} + } + + cmCfg.Issuers = []certmagic.Issuer{cmIssuer} + cmCache := certmagic.NewCache(certmagic.CacheOptions{ + GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) { + return cmCfg, nil + }, + Logger: logger, + }) + cmCfg = certmagic.New(cmCache, *cmCfg) + + if len(c.ACME.Domains) == 0 { + return configError{Field: "acme.domains", Err: errors.New("empty domains")} + } + err := cmCfg.ManageSync(context.Background(), c.ACME.Domains) + if err != nil { + return configError{Field: "acme.domains", Err: err} + } + hyConfig.TLSConfig.GetCertificate = cmCfg.GetCertificate + } + return nil +} + +func genZeroSSLEAB(email string) (*acme.EAB, error) { + req, err := http.NewRequest( + http.MethodPost, + "https://api.zerossl.com/acme/eab-credentials-email", + strings.NewReader(url.Values{"email": []string{email}}.Encode()), + ) + if err != nil { + return nil, fmt.Errorf("failed to creare ZeroSSL EAB request: %w", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("User-Agent", certmagic.UserAgent) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send ZeroSSL EAB request: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + var result struct { + Success bool `json:"success"` + Error struct { + Code int `json:"code"` + Type string `json:"type"` + } `json:"error"` + EABKID string `json:"eab_kid"` + EABHMACKey string `json:"eab_hmac_key"` + } + if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("failed decoding ZeroSSL EAB API response: %w", err) + } + if result.Error.Code != 0 { + return nil, fmt.Errorf("failed getting ZeroSSL EAB credentials: HTTP %d: %s (code %d)", resp.StatusCode, result.Error.Type, result.Error.Code) + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed getting EAB credentials: HTTP %d", resp.StatusCode) + } + + return &acme.EAB{ + KeyID: result.EABKID, + MACKey: result.EABHMACKey, + }, nil +} + +func (c *serverConfig) fillQUICConfig(hyConfig *server.Config) error { + hyConfig.QUICConfig = server.QUICConfig{ + InitialStreamReceiveWindow: c.QUIC.InitStreamReceiveWindow, + MaxStreamReceiveWindow: c.QUIC.MaxStreamReceiveWindow, + InitialConnectionReceiveWindow: c.QUIC.InitConnectionReceiveWindow, + MaxConnectionReceiveWindow: c.QUIC.MaxConnectionReceiveWindow, + MaxIdleTimeout: c.QUIC.MaxIdleTimeout, + MaxIncomingStreams: c.QUIC.MaxIncomingStreams, + DisablePathMTUDiscovery: c.QUIC.DisablePathMTUDiscovery, + } + return nil +} + +func serverConfigOutboundDirectToOutbound(c serverConfigOutboundDirect) (outbounds.PluggableOutbound, error) { + var mode outbounds.DirectOutboundMode + switch strings.ToLower(c.Mode) { + case "", "auto": + mode = outbounds.DirectOutboundModeAuto + case "64": + mode = outbounds.DirectOutboundMode64 + case "46": + mode = outbounds.DirectOutboundMode46 + case "6": + mode = outbounds.DirectOutboundMode6 + case "4": + mode = outbounds.DirectOutboundMode4 + default: + return nil, configError{Field: "outbounds.direct.mode", Err: errors.New("unsupported mode")} + } + bindIP := len(c.BindIPv4) > 0 || len(c.BindIPv6) > 0 + bindDevice := len(c.BindDevice) > 0 + if bindIP && bindDevice { + return nil, configError{Field: "outbounds.direct", Err: errors.New("cannot bind both IP and device")} + } + if bindIP { + ip4, ip6 := net.ParseIP(c.BindIPv4), net.ParseIP(c.BindIPv6) + if len(c.BindIPv4) > 0 && ip4 == nil { + return nil, configError{Field: "outbounds.direct.bindIPv4", Err: errors.New("invalid IPv4 address")} + } + if len(c.BindIPv6) > 0 && ip6 == nil { + return nil, configError{Field: "outbounds.direct.bindIPv6", Err: errors.New("invalid IPv6 address")} + } + return outbounds.NewDirectOutboundBindToIPs(mode, ip4, ip6) + } + if bindDevice { + return outbounds.NewDirectOutboundBindToDevice(mode, c.BindDevice) + } + return outbounds.NewDirectOutboundSimple(mode), nil +} + +func serverConfigOutboundSOCKS5ToOutbound(c serverConfigOutboundSOCKS5) (outbounds.PluggableOutbound, error) { + if c.Addr == "" { + return nil, configError{Field: "outbounds.socks5.addr", Err: errors.New("empty socks5 address")} + } + return outbounds.NewSOCKS5Outbound(c.Addr, c.Username, c.Password), nil +} + +func serverConfigOutboundHTTPToOutbound(c serverConfigOutboundHTTP) (outbounds.PluggableOutbound, error) { + if c.URL == "" { + return nil, configError{Field: "outbounds.http.url", Err: errors.New("empty http address")} + } + return outbounds.NewHTTPOutbound(c.URL, c.Insecure) +} + +func (c *serverConfig) fillRequestHook(hyConfig *server.Config) error { + if c.Sniff.Enable { + s := &sniff.Sniffer{ + Timeout: c.Sniff.Timeout, + RewriteDomain: c.Sniff.RewriteDomain, + } + if c.Sniff.TCPPorts != "" { + s.TCPPorts = eUtils.ParsePortUnion(c.Sniff.TCPPorts) + if s.TCPPorts == nil { + return configError{Field: "sniff.tcpPorts", Err: errors.New("invalid port union")} + } + } + if c.Sniff.UDPPorts != "" { + s.UDPPorts = eUtils.ParsePortUnion(c.Sniff.UDPPorts) + if s.UDPPorts == nil { + return configError{Field: "sniff.udpPorts", Err: errors.New("invalid port union")} + } + } + hyConfig.RequestHook = s + } + return nil +} + +func (c *serverConfig) fillOutboundConfig(hyConfig *server.Config) error { + // Resolver, ACL, actual outbound are all implemented through the Outbound interface. + // Depending on the config, we build a chain like this: + // Resolver(ACL(Outbounds...)) + + // Outbounds + var obs []outbounds.OutboundEntry + if len(c.Outbounds) == 0 { + // Guarantee we have at least one outbound + obs = []outbounds.OutboundEntry{{ + Name: "default", + Outbound: outbounds.NewDirectOutboundSimple(outbounds.DirectOutboundModeAuto), + }} + } else { + obs = make([]outbounds.OutboundEntry, len(c.Outbounds)) + for i, entry := range c.Outbounds { + if entry.Name == "" { + return configError{Field: "outbounds.name", Err: errors.New("empty outbound name")} + } + var ob outbounds.PluggableOutbound + var err error + switch strings.ToLower(entry.Type) { + case "direct": + ob, err = serverConfigOutboundDirectToOutbound(entry.Direct) + case "socks5": + ob, err = serverConfigOutboundSOCKS5ToOutbound(entry.SOCKS5) + case "http": + ob, err = serverConfigOutboundHTTPToOutbound(entry.HTTP) + default: + err = configError{Field: "outbounds.type", Err: errors.New("unsupported outbound type")} + } + if err != nil { + return err + } + obs[i] = outbounds.OutboundEntry{Name: entry.Name, Outbound: ob} + } + } + + var uOb outbounds.PluggableOutbound // "unified" outbound + + // ACL + hasACL := false + if c.ACL.File != "" && len(c.ACL.Inline) > 0 { + return configError{Field: "acl", Err: errors.New("cannot set both acl.file and acl.inline")} + } + gLoader := &utils.GeoLoader{ + GeoIPFilename: c.ACL.GeoIP, + GeoSiteFilename: c.ACL.GeoSite, + UpdateInterval: c.ACL.GeoUpdateInterval, + DownloadFunc: geoDownloadFunc, + DownloadErrFunc: geoDownloadErrFunc, + } + if c.ACL.File != "" { + hasACL = true + acl, err := outbounds.NewACLEngineFromFile(c.ACL.File, obs, gLoader) + if err != nil { + return configError{Field: "acl.file", Err: err} + } + uOb = acl + } else if len(c.ACL.Inline) > 0 { + hasACL = true + acl, err := outbounds.NewACLEngineFromString(strings.Join(c.ACL.Inline, "\n"), obs, gLoader) + if err != nil { + return configError{Field: "acl.inline", Err: err} + } + uOb = acl + } else { + // No ACL, use the first outbound + uOb = obs[0].Outbound + } + + // Resolver + switch strings.ToLower(c.Resolver.Type) { + case "", "system": + if hasACL { + // If the user uses ACL, we must put a resolver in front of it, + // for IP rules to work on domain requests. + uOb = outbounds.NewSystemResolver(uOb) + } + // Otherwise we can just rely on outbound handling on its own. + case "tcp": + if c.Resolver.TCP.Addr == "" { + return configError{Field: "resolver.tcp.addr", Err: errors.New("empty resolver address")} + } + uOb = outbounds.NewStandardResolverTCP(c.Resolver.TCP.Addr, c.Resolver.TCP.Timeout, uOb) + case "udp": + if c.Resolver.UDP.Addr == "" { + return configError{Field: "resolver.udp.addr", Err: errors.New("empty resolver address")} + } + uOb = outbounds.NewStandardResolverUDP(c.Resolver.UDP.Addr, c.Resolver.UDP.Timeout, uOb) + case "tls", "tcp-tls": + if c.Resolver.TLS.Addr == "" { + return configError{Field: "resolver.tls.addr", Err: errors.New("empty resolver address")} + } + uOb = outbounds.NewStandardResolverTLS(c.Resolver.TLS.Addr, c.Resolver.TLS.Timeout, c.Resolver.TLS.SNI, c.Resolver.TLS.Insecure, uOb) + case "https", "http": + if c.Resolver.HTTPS.Addr == "" { + return configError{Field: "resolver.https.addr", Err: errors.New("empty resolver address")} + } + uOb = outbounds.NewDoHResolver(c.Resolver.HTTPS.Addr, c.Resolver.HTTPS.Timeout, c.Resolver.HTTPS.SNI, c.Resolver.HTTPS.Insecure, uOb) + default: + return configError{Field: "resolver.type", Err: errors.New("unsupported resolver type")} + } + + // Speed test + if c.SpeedTest { + uOb = outbounds.NewSpeedtestHandler(uOb) + } + + hyConfig.Outbound = &outbounds.PluggableOutboundAdapter{PluggableOutbound: uOb} + return nil +} + +func (c *serverConfig) fillBandwidthConfig(hyConfig *server.Config) error { + var err error + if c.Bandwidth.Up != "" { + hyConfig.BandwidthConfig.MaxTx, err = utils.ConvBandwidth(c.Bandwidth.Up) + if err != nil { + return configError{Field: "bandwidth.up", Err: err} + } + } + if c.Bandwidth.Down != "" { + hyConfig.BandwidthConfig.MaxRx, err = utils.ConvBandwidth(c.Bandwidth.Down) + if err != nil { + return configError{Field: "bandwidth.down", Err: err} + } + } + return nil +} + +func (c *serverConfig) fillIgnoreClientBandwidth(hyConfig *server.Config) error { + hyConfig.IgnoreClientBandwidth = c.IgnoreClientBandwidth + return nil +} + +func (c *serverConfig) fillDisableUDP(hyConfig *server.Config) error { + hyConfig.DisableUDP = c.DisableUDP + return nil +} + +func (c *serverConfig) fillUDPIdleTimeout(hyConfig *server.Config) error { + hyConfig.UDPIdleTimeout = c.UDPIdleTimeout + return nil +} + +func (c *serverConfig) fillAuthenticator(hyConfig *server.Config) error { + if c.Auth.Type == "" { + return configError{Field: "auth.type", Err: errors.New("empty auth type")} + } + switch strings.ToLower(c.Auth.Type) { + case "password": + if c.Auth.Password == "" { + return configError{Field: "auth.password", Err: errors.New("empty auth password")} + } + hyConfig.Authenticator = &auth.PasswordAuthenticator{Password: c.Auth.Password} + return nil + case "userpass": + if len(c.Auth.UserPass) == 0 { + return configError{Field: "auth.userpass", Err: errors.New("empty auth userpass")} + } + hyConfig.Authenticator = &auth.UserPassAuthenticator{Users: c.Auth.UserPass} + return nil + case "http", "https": + if c.Auth.HTTP.URL == "" { + return configError{Field: "auth.http.url", Err: errors.New("empty auth http url")} + } + hyConfig.Authenticator = auth.NewHTTPAuthenticator(c.Auth.HTTP.URL, c.Auth.HTTP.Insecure) + return nil + case "command", "cmd": + if c.Auth.Command == "" { + return configError{Field: "auth.command", Err: errors.New("empty auth command")} + } + hyConfig.Authenticator = &auth.CommandAuthenticator{Cmd: c.Auth.Command} + return nil + default: + return configError{Field: "auth.type", Err: errors.New("unsupported auth type")} + } +} + +func (c *serverConfig) fillEventLogger(hyConfig *server.Config) error { + hyConfig.EventLogger = &serverLogger{} + return nil +} + +func (c *serverConfig) fillTrafficLogger(hyConfig *server.Config) error { + if c.TrafficStats.Listen != "" { + tss := trafficlogger.NewTrafficStatsServer(c.TrafficStats.Secret) + hyConfig.TrafficLogger = tss + go runTrafficStatsServer(c.TrafficStats.Listen, tss) + } + return nil +} + +// fillMasqHandler must be called after fillConn, as we may need to extract the QUIC +// port number from Conn for MasqTCPServer. +func (c *serverConfig) fillMasqHandler(hyConfig *server.Config) error { + var handler http.Handler + switch strings.ToLower(c.Masquerade.Type) { + case "", "404": + handler = http.NotFoundHandler() + case "file": + if c.Masquerade.File.Dir == "" { + return configError{Field: "masquerade.file.dir", Err: errors.New("empty file directory")} + } + handler = http.FileServer(http.Dir(c.Masquerade.File.Dir)) + case "proxy": + if c.Masquerade.Proxy.URL == "" { + return configError{Field: "masquerade.proxy.url", Err: errors.New("empty proxy url")} + } + u, err := url.Parse(c.Masquerade.Proxy.URL) + if err != nil { + return configError{Field: "masquerade.proxy.url", Err: err} + } + if u.Scheme != "http" && u.Scheme != "https" { + return configError{Field: "masquerade.proxy.url", Err: fmt.Errorf("unsupported protocol scheme \"%s\"", u.Scheme)} + } + handler = &httputil.ReverseProxy{ + Rewrite: func(r *httputil.ProxyRequest) { + r.SetURL(u) + // SetURL rewrites the Host header, + // but we don't want that if rewriteHost is false + if !c.Masquerade.Proxy.RewriteHost { + r.Out.Host = r.In.Host + } + }, + ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { + logger.Error("HTTP reverse proxy error", zap.Error(err)) + w.WriteHeader(http.StatusBadGateway) + }, + } + case "string": + if c.Masquerade.String.Content == "" { + return configError{Field: "masquerade.string.content", Err: errors.New("empty string content")} + } + if c.Masquerade.String.StatusCode != 0 && + (c.Masquerade.String.StatusCode < 200 || + c.Masquerade.String.StatusCode > 599 || + c.Masquerade.String.StatusCode == 233) { + // 233 is reserved for Hysteria authentication + return configError{Field: "masquerade.string.statusCode", Err: errors.New("invalid status code (must be 200-599, except 233)")} + } + handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + for k, v := range c.Masquerade.String.Headers { + w.Header().Set(k, v) + } + if c.Masquerade.String.StatusCode != 0 { + w.WriteHeader(c.Masquerade.String.StatusCode) + } else { + w.WriteHeader(http.StatusOK) // Use 200 OK by default + } + _, _ = w.Write([]byte(c.Masquerade.String.Content)) + }) + default: + return configError{Field: "masquerade.type", Err: errors.New("unsupported masquerade type")} + } + hyConfig.MasqHandler = &masqHandlerLogWrapper{H: handler, QUIC: true} + + if c.Masquerade.ListenHTTP != "" || c.Masquerade.ListenHTTPS != "" { + if c.Masquerade.ListenHTTP != "" && c.Masquerade.ListenHTTPS == "" { + return configError{Field: "masquerade.listenHTTPS", Err: errors.New("having only HTTP server without HTTPS is not supported")} + } + s := masq.MasqTCPServer{ + QUICPort: extractPortFromAddr(hyConfig.Conn.LocalAddr().String()), + HTTPSPort: extractPortFromAddr(c.Masquerade.ListenHTTPS), + Handler: &masqHandlerLogWrapper{H: handler, QUIC: false}, + TLSConfig: &tls.Config{ + Certificates: hyConfig.TLSConfig.Certificates, + GetCertificate: hyConfig.TLSConfig.GetCertificate, + }, + ForceHTTPS: c.Masquerade.ForceHTTPS, + } + go runMasqTCPServer(&s, c.Masquerade.ListenHTTP, c.Masquerade.ListenHTTPS) + } + return nil +} + +// Config validates the fields and returns a ready-to-use Hysteria server config +func (c *serverConfig) Config() (*server.Config, error) { + hyConfig := &server.Config{} + fillers := []func(*server.Config) error{ + c.fillConn, + c.fillTLSConfig, + c.fillQUICConfig, + c.fillRequestHook, + c.fillOutboundConfig, + c.fillBandwidthConfig, + c.fillIgnoreClientBandwidth, + c.fillDisableUDP, + c.fillUDPIdleTimeout, + c.fillAuthenticator, + c.fillEventLogger, + c.fillTrafficLogger, + c.fillMasqHandler, + } + for _, f := range fillers { + if err := f(hyConfig); err != nil { + return nil, err + } + } + + return hyConfig, nil +} + +func runServer(cmd *cobra.Command, args []string) { + logger.Info("server mode") + + if err := viper.ReadInConfig(); err != nil { + logger.Fatal("failed to read server config", zap.Error(err)) + } + var config serverConfig + if err := viper.Unmarshal(&config); err != nil { + logger.Fatal("failed to parse server config", zap.Error(err)) + } + hyConfig, err := config.Config() + if err != nil { + logger.Fatal("failed to load server config", zap.Error(err)) + } + + s, err := server.NewServer(hyConfig) + if err != nil { + logger.Fatal("failed to initialize server", zap.Error(err)) + } + if config.Listen != "" { + logger.Info("server up and running", zap.String("listen", config.Listen)) + } else { + logger.Info("server up and running", zap.String("listen", defaultListenAddr)) + } + + if !disableUpdateCheck { + go runCheckUpdateServer() + } + + if err := s.Serve(); err != nil { + logger.Fatal("failed to serve", zap.Error(err)) + } +} + +func runTrafficStatsServer(listen string, handler http.Handler) { + logger.Info("traffic stats server up and running", zap.String("listen", listen)) + if err := correctnet.HTTPListenAndServe(listen, handler); err != nil { + logger.Fatal("failed to serve traffic stats", zap.Error(err)) + } +} + +func runMasqTCPServer(s *masq.MasqTCPServer, httpAddr, httpsAddr string) { + errChan := make(chan error, 2) + if httpAddr != "" { + go func() { + logger.Info("masquerade HTTP server up and running", zap.String("listen", httpAddr)) + errChan <- s.ListenAndServeHTTP(httpAddr) + }() + } + if httpsAddr != "" { + go func() { + logger.Info("masquerade HTTPS server up and running", zap.String("listen", httpsAddr)) + errChan <- s.ListenAndServeHTTPS(httpsAddr) + }() + } + err := <-errChan + if err != nil { + logger.Fatal("failed to serve masquerade HTTP(S)", zap.Error(err)) + } +} + +func geoDownloadFunc(filename, url string) { + logger.Info("downloading database", zap.String("filename", filename), zap.String("url", url)) +} + +func geoDownloadErrFunc(err error) { + if err != nil { + logger.Error("failed to download database", zap.Error(err)) + } +} + +type serverLogger struct{} + +func (l *serverLogger) Connect(addr net.Addr, id string, tx uint64) { + logger.Info("client connected", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint64("tx", tx)) +} + +func (l *serverLogger) Disconnect(addr net.Addr, id string, err error) { + logger.Info("client disconnected", zap.String("addr", addr.String()), zap.String("id", id), zap.Error(err)) +} + +func (l *serverLogger) TCPRequest(addr net.Addr, id, reqAddr string) { + logger.Debug("TCP request", zap.String("addr", addr.String()), zap.String("id", id), zap.String("reqAddr", reqAddr)) +} + +func (l *serverLogger) TCPError(addr net.Addr, id, reqAddr string, err error) { + if err == nil { + logger.Debug("TCP closed", zap.String("addr", addr.String()), zap.String("id", id), zap.String("reqAddr", reqAddr)) + } else { + logger.Warn("TCP error", zap.String("addr", addr.String()), zap.String("id", id), zap.String("reqAddr", reqAddr), zap.Error(err)) + } +} + +func (l *serverLogger) UDPRequest(addr net.Addr, id string, sessionID uint32, reqAddr string) { + logger.Debug("UDP request", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint32("sessionID", sessionID), zap.String("reqAddr", reqAddr)) +} + +func (l *serverLogger) UDPError(addr net.Addr, id string, sessionID uint32, err error) { + if err == nil { + logger.Debug("UDP closed", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint32("sessionID", sessionID)) + } else { + logger.Warn("UDP error", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint32("sessionID", sessionID), zap.Error(err)) + } +} + +type masqHandlerLogWrapper struct { + H http.Handler + QUIC bool +} + +func (m *masqHandlerLogWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { + logger.Debug("masquerade request", + zap.String("addr", r.RemoteAddr), + zap.String("method", r.Method), + zap.String("host", r.Host), + zap.String("url", r.URL.String()), + zap.Bool("quic", m.QUIC)) + m.H.ServeHTTP(w, r) +} + +func extractPortFromAddr(addr string) int { + _, portStr, err := net.SplitHostPort(addr) + if err != nil { + return 0 + } + port, err := strconv.Atoi(portStr) + if err != nil { + return 0 + } + return port +} diff --git a/v2rayng/hysteria/app/cmd/server_test.go b/v2rayng/hysteria/app/cmd/server_test.go new file mode 100644 index 0000000000..f35edfbd81 --- /dev/null +++ b/v2rayng/hysteria/app/cmd/server_test.go @@ -0,0 +1,187 @@ +package cmd + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/spf13/viper" +) + +// TestServerConfig tests the parsing of the server config +func TestServerConfig(t *testing.T) { + viper.SetConfigFile("server_test.yaml") + err := viper.ReadInConfig() + assert.NoError(t, err) + var config serverConfig + err = viper.Unmarshal(&config) + assert.NoError(t, err) + assert.Equal(t, config, serverConfig{ + Listen: ":8443", + Obfs: serverConfigObfs{ + Type: "salamander", + Salamander: serverConfigObfsSalamander{ + Password: "cry_me_a_r1ver", + }, + }, + TLS: &serverConfigTLS{ + Cert: "some.crt", + Key: "some.key", + SNIGuard: "strict", + }, + ACME: &serverConfigACME{ + Domains: []string{ + "sub1.example.com", + "sub2.example.com", + }, + Email: "haha@cringe.net", + CA: "zero", + ListenHost: "127.0.0.9", + Dir: "random_dir", + Type: "dns", + HTTP: serverConfigACMEHTTP{ + AltPort: 8888, + }, + TLS: serverConfigACMETLS{ + AltPort: 44333, + }, + DNS: serverConfigACMEDNS{ + Name: "gomommy", + Config: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + DisableHTTP: true, + DisableTLSALPN: true, + AltHTTPPort: 8080, + AltTLSALPNPort: 4433, + }, + QUIC: serverConfigQUIC{ + InitStreamReceiveWindow: 77881, + MaxStreamReceiveWindow: 77882, + InitConnectionReceiveWindow: 77883, + MaxConnectionReceiveWindow: 77884, + MaxIdleTimeout: 999 * time.Second, + MaxIncomingStreams: 256, + DisablePathMTUDiscovery: true, + }, + Bandwidth: serverConfigBandwidth{ + Up: "500 mbps", + Down: "100 mbps", + }, + IgnoreClientBandwidth: true, + SpeedTest: true, + DisableUDP: true, + UDPIdleTimeout: 120 * time.Second, + Auth: serverConfigAuth{ + Type: "password", + Password: "goofy_ahh_password", + UserPass: map[string]string{ + "yolo": "swag", + "lol": "kek", + "foo": "bar", + }, + HTTP: serverConfigAuthHTTP{ + URL: "http://127.0.0.1:5000/auth", + Insecure: true, + }, + Command: "/etc/some_command", + }, + Resolver: serverConfigResolver{ + Type: "udp", + TCP: serverConfigResolverTCP{ + Addr: "123.123.123.123:5353", + Timeout: 4 * time.Second, + }, + UDP: serverConfigResolverUDP{ + Addr: "4.6.8.0:53", + Timeout: 2 * time.Second, + }, + TLS: serverConfigResolverTLS{ + Addr: "dot.yolo.com:8853", + Timeout: 10 * time.Second, + SNI: "server1.yolo.net", + Insecure: true, + }, + HTTPS: serverConfigResolverHTTPS{ + Addr: "cringe.ahh.cc", + Timeout: 5 * time.Second, + SNI: "real.stuff.net", + Insecure: true, + }, + }, + Sniff: serverConfigSniff{ + Enable: true, + Timeout: 1 * time.Second, + RewriteDomain: true, + TCPPorts: "80,443,1000-2000", + UDPPorts: "443", + }, + ACL: serverConfigACL{ + File: "chnroute.txt", + Inline: []string{ + "lmao(ok)", + "kek(cringe,boba,tea)", + }, + GeoIP: "some.dat", + GeoSite: "some_site.dat", + GeoUpdateInterval: 168 * time.Hour, + }, + Outbounds: []serverConfigOutboundEntry{ + { + Name: "goodstuff", + Type: "direct", + Direct: serverConfigOutboundDirect{ + Mode: "64", + BindIPv4: "2.4.6.8", + BindIPv6: "0:0:0:0:0:ffff:0204:0608", + BindDevice: "eth233", + }, + }, + { + Name: "badstuff", + Type: "socks5", + SOCKS5: serverConfigOutboundSOCKS5{ + Addr: "shady.proxy.ru:1080", + Username: "hackerman", + Password: "Elliot Alderson", + }, + }, + { + Name: "weirdstuff", + Type: "http", + HTTP: serverConfigOutboundHTTP{ + URL: "https://eyy.lmao:4443/goofy", + Insecure: true, + }, + }, + }, + TrafficStats: serverConfigTrafficStats{ + Listen: ":9999", + Secret: "its_me_mario", + }, + Masquerade: serverConfigMasquerade{ + Type: "proxy", + File: serverConfigMasqueradeFile{ + Dir: "/www/masq", + }, + Proxy: serverConfigMasqueradeProxy{ + URL: "https://some.site.net", + RewriteHost: true, + }, + String: serverConfigMasqueradeString{ + Content: "aint nothin here", + Headers: map[string]string{ + "content-type": "text/plain", + "custom-haha": "lol", + }, + StatusCode: 418, + }, + ListenHTTP: ":80", + ListenHTTPS: ":443", + ForceHTTPS: true, + }, + }) +} diff --git a/v2rayng/hysteria/app/cmd/server_test.yaml b/v2rayng/hysteria/app/cmd/server_test.yaml new file mode 100644 index 0000000000..b7d1a3e7b9 --- /dev/null +++ b/v2rayng/hysteria/app/cmd/server_test.yaml @@ -0,0 +1,142 @@ +listen: :8443 + +obfs: + type: salamander + salamander: + password: cry_me_a_r1ver + +tls: + cert: some.crt + key: some.key + sniGuard: strict + +acme: + domains: + - sub1.example.com + - sub2.example.com + email: haha@cringe.net + ca: zero + listenHost: 127.0.0.9 + dir: random_dir + type: dns + http: + altPort: 8888 + tls: + altPort: 44333 + dns: + name: gomommy + config: + key1: value1 + key2: value2 + disableHTTP: true + disableTLSALPN: true + altHTTPPort: 8080 + altTLSALPNPort: 4433 + +quic: + initStreamReceiveWindow: 77881 + maxStreamReceiveWindow: 77882 + initConnReceiveWindow: 77883 + maxConnReceiveWindow: 77884 + maxIdleTimeout: 999s + maxIncomingStreams: 256 + disablePathMTUDiscovery: true + +bandwidth: + up: 500 mbps + down: 100 mbps + +ignoreClientBandwidth: true + +speedTest: true + +disableUDP: true +udpIdleTimeout: 120s + +auth: + type: password + password: goofy_ahh_password + userpass: + yolo: swag + lol: kek + foo: bar + http: + url: http://127.0.0.1:5000/auth + insecure: true + command: /etc/some_command + +resolver: + type: udp + tcp: + addr: 123.123.123.123:5353 + timeout: 4s + udp: + addr: 4.6.8.0:53 + timeout: 2s + tls: + addr: dot.yolo.com:8853 + timeout: 10s + sni: server1.yolo.net + insecure: true + https: + addr: cringe.ahh.cc + timeout: 5s + sni: real.stuff.net + insecure: true + +sniff: + enable: true + timeout: 1s + rewriteDomain: true + tcpPorts: 80,443,1000-2000 + udpPorts: 443 + +acl: + file: chnroute.txt + inline: + - lmao(ok) + - kek(cringe,boba,tea) + geoip: some.dat + geosite: some_site.dat + geoUpdateInterval: 168h + +outbounds: + - name: goodstuff + type: direct + direct: + mode: 64 + bindIPv4: 2.4.6.8 + bindIPv6: 0:0:0:0:0:ffff:0204:0608 + bindDevice: eth233 + - name: badstuff + type: socks5 + socks5: + addr: shady.proxy.ru:1080 + username: hackerman + password: Elliot Alderson + - name: weirdstuff + type: http + http: + url: https://eyy.lmao:4443/goofy + insecure: true + +trafficStats: + listen: :9999 + secret: its_me_mario + +masquerade: + type: proxy + file: + dir: /www/masq + proxy: + url: https://some.site.net + rewriteHost: true + string: + content: aint nothin here + headers: + content-type: text/plain + custom-haha: lol + statusCode: 418 + listenHTTP: :80 + listenHTTPS: :443 + forceHTTPS: true diff --git a/v2rayng/hysteria/app/cmd/share.go b/v2rayng/hysteria/app/cmd/share.go new file mode 100644 index 0000000000..ad96e80cc2 --- /dev/null +++ b/v2rayng/hysteria/app/cmd/share.go @@ -0,0 +1,55 @@ +package cmd + +import ( + "fmt" + + "github.com/apernet/hysteria/app/v2/internal/utils" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/zap" +) + +var ( + noText bool + withQR bool +) + +// shareCmd represents the share command +var shareCmd = &cobra.Command{ + Use: "share", + Short: "Generate share URI", + Long: "Generate a hysteria2:// URI from a client config for sharing", + Run: runShare, +} + +func init() { + initShareFlags() + rootCmd.AddCommand(shareCmd) +} + +func initShareFlags() { + shareCmd.Flags().BoolVar(&noText, "notext", false, "do not show URI as text") + shareCmd.Flags().BoolVar(&withQR, "qr", false, "show URI as QR code") +} + +func runShare(cmd *cobra.Command, args []string) { + if err := viper.ReadInConfig(); err != nil { + logger.Fatal("failed to read client config", zap.Error(err)) + } + var config clientConfig + if err := viper.Unmarshal(&config); err != nil { + logger.Fatal("failed to parse client config", zap.Error(err)) + } + if _, err := config.Config(); err != nil { + logger.Fatal("failed to load client config", zap.Error(err)) + } + + u := config.URI() + + if !noText { + fmt.Println(u) + } + if withQR { + utils.PrintQR(u) + } +} diff --git a/v2rayng/hysteria/app/cmd/speedtest.go b/v2rayng/hysteria/app/cmd/speedtest.go new file mode 100644 index 0000000000..f5c37fb81a --- /dev/null +++ b/v2rayng/hysteria/app/cmd/speedtest.go @@ -0,0 +1,178 @@ +package cmd + +import ( + "errors" + "fmt" + "os" + "os/signal" + "syscall" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/zap" + + "github.com/apernet/hysteria/core/v2/client" + hyErrors "github.com/apernet/hysteria/core/v2/errors" + "github.com/apernet/hysteria/extras/v2/outbounds" + "github.com/apernet/hysteria/extras/v2/outbounds/speedtest" +) + +var ( + skipDownload bool + skipUpload bool + dataSize uint32 + useBytes bool + + speedtestAddr = fmt.Sprintf("%s:%d", outbounds.SpeedtestDest, 0) +) + +// speedtestCmd represents the speedtest command +var speedtestCmd = &cobra.Command{ + Use: "speedtest", + Short: "Speed test mode", + Long: "Perform a speed test through the proxy server. The server must have speed test support enabled.", + Run: runSpeedtest, +} + +func init() { + initSpeedtestFlags() + rootCmd.AddCommand(speedtestCmd) +} + +func initSpeedtestFlags() { + speedtestCmd.Flags().BoolVar(&skipDownload, "skip-download", false, "Skip download test") + speedtestCmd.Flags().BoolVar(&skipUpload, "skip-upload", false, "Skip upload test") + speedtestCmd.Flags().Uint32Var(&dataSize, "data-size", 1024*1024*100, "Data size for download and upload tests") + speedtestCmd.Flags().BoolVar(&useBytes, "use-bytes", false, "Use bytes per second instead of bits per second") +} + +func runSpeedtest(cmd *cobra.Command, args []string) { + logger.Info("speed test mode") + + if err := viper.ReadInConfig(); err != nil { + logger.Fatal("failed to read client config", zap.Error(err)) + } + var config clientConfig + if err := viper.Unmarshal(&config); err != nil { + logger.Fatal("failed to parse client config", zap.Error(err)) + } + hyConfig, err := config.Config() + if err != nil { + logger.Fatal("failed to load client config", zap.Error(err)) + } + + c, info, err := client.NewClient(hyConfig) + if err != nil { + logger.Fatal("failed to initialize client", zap.Error(err)) + } + defer c.Close() + logger.Info("connected to server", + zap.Bool("udpEnabled", info.UDPEnabled), + zap.Uint64("tx", info.Tx)) + + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) + defer signal.Stop(signalChan) + + runChan := make(chan struct{}, 1) + go func() { + if !skipDownload { + runDownloadTest(c) + } + if !skipUpload { + runUploadTest(c) + } + runChan <- struct{}{} + }() + + select { + case <-signalChan: + logger.Info("received signal, shutting down gracefully") + case <-runChan: + logger.Info("speed test complete") + } +} + +func runDownloadTest(c client.Client) { + logger.Info("performing download test") + downConn, err := c.TCP(speedtestAddr) + if err != nil { + if errors.As(err, &hyErrors.DialError{}) { + logger.Fatal("failed to connect (server may not support speed test)", zap.Error(err)) + } else { + logger.Fatal("failed to connect", zap.Error(err)) + } + } + defer downConn.Close() + + downClient := &speedtest.Client{Conn: downConn} + currentTotal := uint32(0) + err = downClient.Download(dataSize, func(d time.Duration, b uint32, done bool) { + if !done { + currentTotal += b + logger.Info("downloading", + zap.Uint32("bytes", b), + zap.String("progress", fmt.Sprintf("%.2f%%", float64(currentTotal)/float64(dataSize)*100)), + zap.String("speed", formatSpeed(b, d, useBytes))) + } else { + logger.Info("download complete", + zap.Uint32("bytes", b), + zap.String("speed", formatSpeed(b, d, useBytes))) + } + }) + if err != nil { + logger.Fatal("download test failed", zap.Error(err)) + } + logger.Info("download test complete") +} + +func runUploadTest(c client.Client) { + logger.Info("performing upload test") + upConn, err := c.TCP(speedtestAddr) + if err != nil { + if errors.As(err, &hyErrors.DialError{}) { + logger.Fatal("failed to connect (server may not support speed test)", zap.Error(err)) + } else { + logger.Fatal("failed to connect", zap.Error(err)) + } + } + defer upConn.Close() + + upClient := &speedtest.Client{Conn: upConn} + currentTotal := uint32(0) + err = upClient.Upload(dataSize, func(d time.Duration, b uint32, done bool) { + if !done { + currentTotal += b + logger.Info("uploading", + zap.Uint32("bytes", b), + zap.String("progress", fmt.Sprintf("%.2f%%", float64(currentTotal)/float64(dataSize)*100)), + zap.String("speed", formatSpeed(b, d, useBytes))) + } else { + logger.Info("upload complete", + zap.Uint32("bytes", b), + zap.String("speed", formatSpeed(b, d, useBytes))) + } + }) + if err != nil { + logger.Fatal("upload test failed", zap.Error(err)) + } + logger.Info("upload test complete") +} + +func formatSpeed(bytes uint32, duration time.Duration, useBytes bool) string { + speed := float64(bytes) / duration.Seconds() + var units []string + if useBytes { + units = []string{"B/s", "KB/s", "MB/s", "GB/s"} + } else { + units = []string{"bps", "Kbps", "Mbps", "Gbps"} + speed *= 8 + } + unitIndex := 0 + for speed > 1000 && unitIndex < len(units)-1 { + speed /= 1000 + unitIndex++ + } + return fmt.Sprintf("%.2f %s", speed, units[unitIndex]) +} diff --git a/v2rayng/hysteria/app/cmd/update.go b/v2rayng/hysteria/app/cmd/update.go new file mode 100644 index 0000000000..11eadd0f77 --- /dev/null +++ b/v2rayng/hysteria/app/cmd/update.go @@ -0,0 +1,88 @@ +package cmd + +import ( + "time" + + "github.com/spf13/cobra" + "go.uber.org/zap" + + "github.com/apernet/hysteria/app/v2/internal/utils" + "github.com/apernet/hysteria/core/v2/client" +) + +const ( + updateCheckInterval = 24 * time.Hour +) + +// checkUpdateCmd represents the checkUpdate command +var checkUpdateCmd = &cobra.Command{ + Use: "check-update", + Short: "Check for updates", + Long: "Check for updates.", + Run: runCheckUpdate, +} + +func init() { + rootCmd.AddCommand(checkUpdateCmd) +} + +func runCheckUpdate(cmd *cobra.Command, args []string) { + logger.Info("checking for updates", + zap.String("version", appVersion), + zap.String("platform", appPlatform), + zap.String("arch", appArch), + zap.String("channel", appType), + ) + + checker := utils.NewServerUpdateChecker(appVersion, appPlatform, appArch, appType) + resp, err := checker.Check() + if err != nil { + logger.Fatal("failed to check for updates", zap.Error(err)) + } + if resp.HasUpdate { + logger.Info("update available", + zap.String("version", resp.LatestVersion), + zap.String("url", resp.URL), + zap.Bool("urgent", resp.Urgent), + ) + } else { + logger.Info("no update available") + } +} + +// runCheckUpdateServer is the background update checking routine for server mode +func runCheckUpdateServer() { + checker := utils.NewServerUpdateChecker(appVersion, appPlatform, appArch, appType) + checkUpdateRoutine(checker) +} + +// runCheckUpdateClient is the background update checking routine for client mode +func runCheckUpdateClient(hyClient client.Client) { + checker := utils.NewClientUpdateChecker(appVersion, appPlatform, appArch, appType, hyClient) + checkUpdateRoutine(checker) +} + +func checkUpdateRoutine(checker *utils.UpdateChecker) { + ticker := time.NewTicker(updateCheckInterval) + for { + logger.Debug("checking for updates", + zap.String("version", appVersion), + zap.String("platform", appPlatform), + zap.String("arch", appArch), + zap.String("channel", appType), + ) + resp, err := checker.Check() + if err != nil { + logger.Debug("failed to check for updates", zap.Error(err)) + } else if resp.HasUpdate { + logger.Info("update available", + zap.String("version", resp.LatestVersion), + zap.String("url", resp.URL), + zap.Bool("urgent", resp.Urgent), + ) + } else { + logger.Debug("no update available") + } + <-ticker.C + } +} diff --git a/v2rayng/hysteria/app/cmd/version.go b/v2rayng/hysteria/app/cmd/version.go new file mode 100644 index 0000000000..091aae1f0c --- /dev/null +++ b/v2rayng/hysteria/app/cmd/version.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// versionCmd represents the version command +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Show version", + Long: "Show version.", + Run: runVersion, +} + +func init() { + rootCmd.AddCommand(versionCmd) +} + +func runVersion(cmd *cobra.Command, args []string) { + fmt.Println(appAboutLong) +} diff --git a/v2rayng/hysteria/app/go.mod b/v2rayng/hysteria/app/go.mod new file mode 100644 index 0000000000..dab7e51776 --- /dev/null +++ b/v2rayng/hysteria/app/go.mod @@ -0,0 +1,90 @@ +module github.com/apernet/hysteria/app/v2 + +go 1.22 + +toolchain go1.23.2 + +require ( + github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f + github.com/apernet/hysteria/core/v2 v2.0.0-00010101000000-000000000000 + github.com/apernet/hysteria/extras/v2 v2.0.0-00010101000000-000000000000 + github.com/apernet/sing-tun v0.2.6-0.20240323130332-b9f6511036ad + github.com/caddyserver/certmagic v0.17.2 + github.com/libdns/cloudflare v0.1.1 + github.com/libdns/duckdns v0.2.0 + github.com/libdns/gandi v1.0.3 + github.com/libdns/godaddy v1.0.3 + github.com/libdns/namedotcom v0.3.3 + github.com/libdns/vultr v1.0.0 + github.com/mdp/qrterminal/v3 v3.1.1 + github.com/mholt/acmez v1.0.4 + github.com/sagernet/sing v0.3.2 + github.com/spf13/cobra v1.7.0 + github.com/spf13/viper v1.15.0 + github.com/stretchr/testify v1.9.0 + github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 + go.uber.org/zap v1.24.0 + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 + golang.org/x/sys v0.23.0 +) + +require ( + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/apernet/quic-go v0.48.2-0.20241104191913-cb103fcecfe7 // indirect + github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 // indirect + github.com/cloudflare/circl v1.3.9 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.6 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/klauspost/cpuid/v2 v2.1.1 // indirect + github.com/libdns/libdns v0.2.2 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/miekg/dns v1.1.59 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/onsi/ginkgo/v2 v2.9.5 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/pelletier/go-toml/v2 v2.0.6 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/refraction-networking/utls v1.6.6 // indirect + github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect + github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect + github.com/spf13/afero v1.9.3 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/subosito/gotenv v1.4.2 // indirect + github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect + github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect + github.com/vultr/govultr/v3 v3.6.4 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/mock v0.4.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/oauth2 v0.20.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/text v0.17.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + rsc.io/qr v0.2.0 // indirect +) + +replace github.com/apernet/hysteria/core/v2 => ../core + +replace github.com/apernet/hysteria/extras/v2 => ../extras diff --git a/v2rayng/hysteria/app/go.sum b/v2rayng/hysteria/app/go.sum new file mode 100644 index 0000000000..dec519bcc1 --- /dev/null +++ b/v2rayng/hysteria/app/go.sum @@ -0,0 +1,657 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f h1:uVh0qpEslrWjgzx9vOcyCqsOY3c9kofDZ1n+qaw35ZY= +github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f/go.mod h1:xkkq9D4ygcldQQhKS/w9CadiCKwCngU7K9E3DaKahpM= +github.com/apernet/quic-go v0.48.2-0.20241104191913-cb103fcecfe7 h1:zO38yBOvQ1dLHbSuaU5BFZ8zalnSDQslj+i/9AGOk9s= +github.com/apernet/quic-go v0.48.2-0.20241104191913-cb103fcecfe7/go.mod h1:LoSUY2chVqNQCDyi4IZGqPpXLy1FuCkE37PKwtJvNGg= +github.com/apernet/sing-tun v0.2.6-0.20240323130332-b9f6511036ad h1:QzQ2sKpc9o42HNRR8ukM5uMC/RzR2HgZd/Nvaqol2C0= +github.com/apernet/sing-tun v0.2.6-0.20240323130332-b9f6511036ad/go.mod h1:S5IydyLSN/QAfvY+r2GoomPJ6hidtXWm/Ad18sJVssk= +github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0= +github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/caddyserver/certmagic v0.17.2 h1:o30seC1T/dBqBCNNGNHWwj2i5/I/FMjBbTAhjADP3nE= +github.com/caddyserver/certmagic v0.17.2/go.mod h1:ouWUuC490GOLJzkyN35eXfV8bSbwMwSf4bdhkIxtdQE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= +github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-retryablehttp v0.7.6 h1:TwRYfx2z2C4cLbXmT8I5PgP/xmuqASDyiVuGYfs9GZM= +github.com/hashicorp/go-retryablehttp v0.7.6/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= +github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.1.1 h1:t0wUqjowdm8ezddV5k0tLWVklVuvLJpoHeb4WBdydm0= +github.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054= +github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU= +github.com/libdns/duckdns v0.2.0 h1:vd3pE09G2qTx1Zh1o3LmrivWSByD3Z5FbL7csX5vDgE= +github.com/libdns/duckdns v0.2.0/go.mod h1:jCQ/7+qvhLK39+28qXvKEYGBBvmHBCmIwNqdJTCUmVs= +github.com/libdns/gandi v1.0.3 h1:FIvipWOg/O4zi75fPRmtcolRKqI6MgrbpFy2p5KYdUk= +github.com/libdns/gandi v1.0.3/go.mod h1:G6dw58Xnji2xX+lb+uZxGbtmfxKllm1CGHE2bOPG3WA= +github.com/libdns/godaddy v1.0.3 h1:PX1FOYDQ1HGQzz8mVOmtwm3aa6Sv5MwCkNzivUUTA44= +github.com/libdns/godaddy v1.0.3/go.mod h1:vuKWUXnvblDvcaiRwutOoLl7DuB21x8tI06owsF/JTM= +github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= +github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= +github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +github.com/libdns/namedotcom v0.3.3 h1:R10C7+IqQGVeC4opHHMiFNBxdNBg1bi65ZwqLESl+jE= +github.com/libdns/namedotcom v0.3.3/go.mod h1:GbYzsAF2yRUpI0WgIK5fs5UX+kDVUPaYCFLpTnKQm0s= +github.com/libdns/vultr v1.0.0 h1:W8B4+k2bm9ro3bZLSZV9hMOQI+uO6Svu+GmD+Olz7ZI= +github.com/libdns/vultr v1.0.0/go.mod h1:8K1HJExcbeHS4YPkFHRZpqpXZzZ+DZAA0m0VikJgEqk= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mdp/qrterminal/v3 v3.1.1 h1:cIPwg3QU0OIm9+ce/lRfWXhPwEjOSKwk3HBwL3HBTyc= +github.com/mdp/qrterminal/v3 v3.1.1/go.mod h1:5lJlXe7Jdr8wlPDdcsJttv1/knsRgzXASyr4dcGZqNU= +github.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80= +github.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY= +github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c= +github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= +github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= +github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig= +github.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE= +github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= +github.com/sagernet/sing v0.3.2 h1:CwWcxUBPkMvwgfe2/zUgY5oHG9qOL8Aob/evIFYK9jo= +github.com/sagernet/sing v0.3.2/go.mod h1:qHySJ7u8po9DABtMYEkNBcOumx7ZZJf/fbv2sfTkNHE= +github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg= +github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s= +github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= +github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= +github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0= +github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM= +github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 h1:d/Wr/Vl/wiJHc3AHYbYs5I3PucJvRuw3SvbmlIRf+oM= +github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM= +github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= +github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vultr/govultr/v3 v3.6.4 h1:unvY9eXlBw667ECQZDbBDOIaWB8wkk6Bx+yB0IMKXJ4= +github.com/vultr/govultr/v3 v3.6.4/go.mod h1:rt9v2x114jZmmLAE/h5N5jnxTmsK9ewwS2oQZ0UBQzM= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= +golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= +rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/v2rayng/hysteria/app/internal/forwarding/tcp.go b/v2rayng/hysteria/app/internal/forwarding/tcp.go new file mode 100644 index 0000000000..8936385d77 --- /dev/null +++ b/v2rayng/hysteria/app/internal/forwarding/tcp.go @@ -0,0 +1,62 @@ +package forwarding + +import ( + "io" + "net" + + "github.com/apernet/hysteria/core/v2/client" +) + +type TCPTunnel struct { + HyClient client.Client + Remote string + EventLogger TCPEventLogger +} + +type TCPEventLogger interface { + Connect(addr net.Addr) + Error(addr net.Addr, err error) +} + +func (t *TCPTunnel) Serve(listener net.Listener) error { + for { + conn, err := listener.Accept() + if err != nil { + return err + } + go t.handle(conn) + } +} + +func (t *TCPTunnel) handle(conn net.Conn) { + defer conn.Close() + + if t.EventLogger != nil { + t.EventLogger.Connect(conn.RemoteAddr()) + } + var closeErr error + defer func() { + if t.EventLogger != nil { + t.EventLogger.Error(conn.RemoteAddr(), closeErr) + } + }() + + rc, err := t.HyClient.TCP(t.Remote) + if err != nil { + closeErr = err + return + } + defer rc.Close() + + // Start forwarding + copyErrChan := make(chan error, 2) + go func() { + _, copyErr := io.Copy(rc, conn) + copyErrChan <- copyErr + }() + go func() { + _, copyErr := io.Copy(conn, rc) + copyErrChan <- copyErr + }() + closeErr = <-copyErrChan +} diff --git a/v2rayng/hysteria/app/internal/forwarding/tcp_test.go b/v2rayng/hysteria/app/internal/forwarding/tcp_test.go new file mode 100644 index 0000000000..075b233be7 --- /dev/null +++ b/v2rayng/hysteria/app/internal/forwarding/tcp_test.go @@ -0,0 +1,39 @@ +package forwarding + +import ( + "crypto/rand" + "net" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/apernet/hysteria/app/v2/internal/utils_test" +) + +func TestTCPTunnel(t *testing.T) { + // Start the tunnel + l, err := net.Listen("tcp", "127.0.0.1:34567") + assert.NoError(t, err) + defer l.Close() + tunnel := &TCPTunnel{ + HyClient: &utils_test.MockEchoHyClient{}, + } + go tunnel.Serve(l) + + for i := 0; i < 10; i++ { + conn, err := net.Dial("tcp", "127.0.0.1:34567") + assert.NoError(t, err) + + data := make([]byte, 1024) + _, _ = rand.Read(data) + _, err = conn.Write(data) + assert.NoError(t, err) + + recv := make([]byte, 1024) + _, err = conn.Read(recv) + assert.NoError(t, err) + + assert.Equal(t, data, recv) + _ = conn.Close() + } +} diff --git a/v2rayng/hysteria/app/internal/forwarding/udp.go b/v2rayng/hysteria/app/internal/forwarding/udp.go new file mode 100644 index 0000000000..35886a8f33 --- /dev/null +++ b/v2rayng/hysteria/app/internal/forwarding/udp.go @@ -0,0 +1,180 @@ +package forwarding + +import ( + "net" + "sync" + "sync/atomic" + "time" + + "github.com/apernet/hysteria/core/v2/client" +) + +const ( + udpBufferSize = 4096 + + defaultTimeout = 60 * time.Second + idleCleanupInterval = 1 * time.Second +) + +type atomicTime struct { + v atomic.Value +} + +func newAtomicTime(t time.Time) *atomicTime { + a := &atomicTime{} + a.Set(t) + return a +} + +func (t *atomicTime) Set(new time.Time) { + t.v.Store(new) +} + +func (t *atomicTime) Get() time.Time { + return t.v.Load().(time.Time) +} + +type sessionEntry struct { + HyConn client.HyUDPConn + Last *atomicTime + Timeout bool // true if the session is closed due to timeout +} + +func (e *sessionEntry) Feed(data []byte, addr string) error { + e.Last.Set(time.Now()) + return e.HyConn.Send(data, addr) +} + +func (e *sessionEntry) ReceiveLoop(pc net.PacketConn, addr net.Addr) error { + for { + data, _, err := e.HyConn.Receive() + if err != nil { + return err + } + _, err = pc.WriteTo(data, addr) + if err != nil { + return err + } + e.Last.Set(time.Now()) + } +} + +type UDPTunnel struct { + HyClient client.Client + Remote string + Timeout time.Duration + EventLogger UDPEventLogger + + m map[string]*sessionEntry // addr -> HyConn + mutex sync.RWMutex +} + +type UDPEventLogger interface { + Connect(addr net.Addr) + Error(addr net.Addr, err error) +} + +func (t *UDPTunnel) Serve(pc net.PacketConn) error { + t.m = make(map[string]*sessionEntry) + + stopCh := make(chan struct{}) + go t.idleCleanupLoop(stopCh) + defer close(stopCh) + defer t.cleanup(false) + + buf := make([]byte, udpBufferSize) + for { + n, addr, err := pc.ReadFrom(buf) + if err != nil { + return err + } + t.feed(pc, addr, buf[:n]) + } +} + +func (t *UDPTunnel) idleCleanupLoop(stopCh <-chan struct{}) { + ticker := time.NewTicker(idleCleanupInterval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + t.cleanup(true) + case <-stopCh: + return + } + } +} + +func (t *UDPTunnel) cleanup(idleOnly bool) { + // We use RLock here as we are only scanning the map, not deleting from it. + t.mutex.RLock() + defer t.mutex.RUnlock() + + timeout := t.Timeout + if timeout == 0 { + timeout = defaultTimeout + } + + now := time.Now() + for _, entry := range t.m { + if !idleOnly || now.Sub(entry.Last.Get()) > timeout { + entry.Timeout = true + _ = entry.HyConn.Close() + // Closing the connection here will cause the ReceiveLoop to exit, + // and the session will be removed from the map there. + } + } +} + +func (t *UDPTunnel) feed(pc net.PacketConn, addr net.Addr, data []byte) { + t.mutex.RLock() + entry := t.m[addr.String()] + t.mutex.RUnlock() + + // Create a new session if not exists + if entry == nil { + if t.EventLogger != nil { + t.EventLogger.Connect(addr) + } + hyConn, err := t.HyClient.UDP() + if err != nil { + if t.EventLogger != nil { + t.EventLogger.Error(addr, err) + } + return + } + entry = &sessionEntry{ + HyConn: hyConn, + Last: newAtomicTime(time.Now()), + } + // Start the receive loop for this session + // Local <- Remote + go func() { + err := entry.ReceiveLoop(pc, addr) + if !entry.Timeout { + _ = hyConn.Close() + if t.EventLogger != nil { + t.EventLogger.Error(addr, err) + } + } else { + // Connection already closed by timeout cleanup, + // no need to close again here. + // Use nil error to indicate timeout. + if t.EventLogger != nil { + t.EventLogger.Error(addr, nil) + } + } + // Remove the session from the map + t.mutex.Lock() + delete(t.m, addr.String()) + t.mutex.Unlock() + }() + // Insert the session into the map + t.mutex.Lock() + t.m[addr.String()] = entry + t.mutex.Unlock() + } + + // Feed the message to the session + _ = entry.Feed(data, t.Remote) +} diff --git a/v2rayng/hysteria/app/internal/forwarding/udp_test.go b/v2rayng/hysteria/app/internal/forwarding/udp_test.go new file mode 100644 index 0000000000..ba4f3baed2 --- /dev/null +++ b/v2rayng/hysteria/app/internal/forwarding/udp_test.go @@ -0,0 +1,39 @@ +package forwarding + +import ( + "crypto/rand" + "net" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/apernet/hysteria/app/v2/internal/utils_test" +) + +func TestUDPTunnel(t *testing.T) { + // Start the tunnel + l, err := net.ListenPacket("udp", "127.0.0.1:34567") + assert.NoError(t, err) + defer l.Close() + tunnel := &UDPTunnel{ + HyClient: &utils_test.MockEchoHyClient{}, + } + go tunnel.Serve(l) + + for i := 0; i < 10; i++ { + conn, err := net.Dial("udp", "127.0.0.1:34567") + assert.NoError(t, err) + + data := make([]byte, 1024) + _, _ = rand.Read(data) + _, err = conn.Write(data) + assert.NoError(t, err) + + recv := make([]byte, 1024) + _, err = conn.Read(recv) + assert.NoError(t, err) + + assert.Equal(t, data, recv) + _ = conn.Close() + } +} diff --git a/v2rayng/hysteria/app/internal/http/server.go b/v2rayng/hysteria/app/internal/http/server.go new file mode 100644 index 0000000000..0b5e411665 --- /dev/null +++ b/v2rayng/hysteria/app/internal/http/server.go @@ -0,0 +1,301 @@ +package http + +import ( + "bufio" + "bytes" + "context" + "encoding/base64" + "fmt" + "io" + "net" + "net/http" + "strings" + "time" + + "github.com/apernet/hysteria/core/v2/client" +) + +const ( + httpClientTimeout = 10 * time.Second +) + +// Server is an HTTP server using a Hysteria client as outbound. +type Server struct { + HyClient client.Client + AuthFunc func(username, password string) bool // nil = no authentication + AuthRealm string + EventLogger EventLogger + + httpClient *http.Client +} + +type EventLogger interface { + ConnectRequest(addr net.Addr, reqAddr string) + ConnectError(addr net.Addr, reqAddr string, err error) + HTTPRequest(addr net.Addr, reqURL string) + HTTPError(addr net.Addr, reqURL string, err error) +} + +func (s *Server) Serve(listener net.Listener) error { + for { + conn, err := listener.Accept() + if err != nil { + return err + } + go s.dispatch(conn) + } +} + +func (s *Server) dispatch(conn net.Conn) { + bufReader := bufio.NewReader(conn) + for { + req, err := http.ReadRequest(bufReader) + if err != nil { + // Connection error or invalid request + _ = conn.Close() + return + } + if s.AuthFunc != nil { + authOK := false + // Check the Proxy-Authorization header + pAuth := req.Header.Get("Proxy-Authorization") + if strings.HasPrefix(pAuth, "Basic ") { + userPass, err := base64.URLEncoding.DecodeString(pAuth[6:]) + if err == nil { + userPassParts := strings.SplitN(string(userPass), ":", 2) + if len(userPassParts) == 2 { + authOK = s.AuthFunc(userPassParts[0], userPassParts[1]) + } + } + } + if !authOK { + // Proxy authentication required + _ = sendProxyAuthRequired(conn, req, s.AuthRealm) + _ = conn.Close() + return + } + } + if req.Method == http.MethodConnect { + if bufReader.Buffered() > 0 { + // There is still data in the buffered reader. + // We need to get it out and put it into a cachedConn, + // so that handleConnect can read it. + data := make([]byte, bufReader.Buffered()) + _, err := io.ReadFull(bufReader, data) + if err != nil { + // Read from buffer failed, is this possible? + _ = conn.Close() + return + } + cachedConn := &cachedConn{ + Conn: conn, + Buffer: *bytes.NewBuffer(data), + } + s.handleConnect(cachedConn, req) + } else { + // No data in the buffered reader, we can just pass the original connection. + s.handleConnect(conn, req) + } + // handleConnect will take over the connection, + // i.e. it will not return until the connection is closed. + // When it returns, there will be no more requests from this connection, + // so we simply exit the loop. + return + } else { + // handleRequest on the other hand handles one request at a time, + // and returns when the request is done. It returns a bool indicating + // whether the connection should be kept alive, but itself never closes + // the connection. + keepAlive := s.handleRequest(conn, req) + if !keepAlive { + _ = conn.Close() + return + } + } + } +} + +// cachedConn is a net.Conn wrapper that first Read()s from a buffer, +// and then from the underlying net.Conn when the buffer is drained. +type cachedConn struct { + net.Conn + Buffer bytes.Buffer +} + +func (c *cachedConn) Read(b []byte) (int, error) { + if c.Buffer.Len() > 0 { + n, err := c.Buffer.Read(b) + if err == io.EOF { + // Buffer is drained, hide it from the caller + err = nil + } + return n, err + } + return c.Conn.Read(b) +} + +func (s *Server) handleConnect(conn net.Conn, req *http.Request) { + defer conn.Close() + + port := req.URL.Port() + if port == "" { + // HTTP defaults to port 80 + port = "80" + } + reqAddr := net.JoinHostPort(req.URL.Hostname(), port) + + // Connect request & error log + if s.EventLogger != nil { + s.EventLogger.ConnectRequest(conn.RemoteAddr(), reqAddr) + } + var closeErr error + defer func() { + if s.EventLogger != nil { + s.EventLogger.ConnectError(conn.RemoteAddr(), reqAddr, closeErr) + } + }() + + // Dial + rConn, err := s.HyClient.TCP(reqAddr) + if err != nil { + _ = sendSimpleResponse(conn, req, http.StatusBadGateway) + closeErr = err + return + } + defer rConn.Close() + + // Send 200 OK response and start relaying + _ = sendSimpleResponse(conn, req, http.StatusOK) + copyErrChan := make(chan error, 2) + go func() { + _, err := io.Copy(rConn, conn) + copyErrChan <- err + }() + go func() { + _, err := io.Copy(conn, rConn) + copyErrChan <- err + }() + closeErr = <-copyErrChan +} + +func (s *Server) handleRequest(conn net.Conn, req *http.Request) bool { + // Some clients use Connection, some use Proxy-Connection + // https://www.oreilly.com/library/view/http-the-definitive/1565925092/re40.html + keepAlive := req.ProtoAtLeast(1, 1) && + (strings.ToLower(req.Header.Get("Proxy-Connection")) == "keep-alive" || + strings.ToLower(req.Header.Get("Connection")) == "keep-alive") + req.RequestURI = "" // Outgoing request should not have RequestURI + + removeHopByHopHeaders(req.Header) + removeExtraHTTPHostPort(req) + + if req.URL.Scheme == "" || req.URL.Host == "" { + _ = sendSimpleResponse(conn, req, http.StatusBadRequest) + return false + } + + // Request & error log + if s.EventLogger != nil { + s.EventLogger.HTTPRequest(conn.RemoteAddr(), req.URL.String()) + } + var closeErr error + defer func() { + if s.EventLogger != nil { + s.EventLogger.HTTPError(conn.RemoteAddr(), req.URL.String(), closeErr) + } + }() + + if s.httpClient == nil { + s.initHTTPClient() + } + + // Do the request and send the response back + resp, err := s.httpClient.Do(req) + if err != nil { + closeErr = err + _ = sendSimpleResponse(conn, req, http.StatusBadGateway) + return false + } + + removeHopByHopHeaders(resp.Header) + if keepAlive { + resp.Header.Set("Connection", "keep-alive") + resp.Header.Set("Proxy-Connection", "keep-alive") + resp.Header.Set("Keep-Alive", "timeout=60") + } + + closeErr = resp.Write(conn) + return closeErr == nil && keepAlive +} + +func (s *Server) initHTTPClient() { + s.httpClient = &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + // HyClient doesn't support context for now + return s.HyClient.TCP(addr) + }, + }, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + Timeout: httpClientTimeout, + } +} + +func removeHopByHopHeaders(header http.Header) { + header.Del("Proxy-Connection") // Not in RFC but common + // https://www.ietf.org/rfc/rfc2616.txt + header.Del("Connection") + header.Del("Keep-Alive") + header.Del("Proxy-Authenticate") + header.Del("Proxy-Authorization") + header.Del("TE") + header.Del("Trailers") + header.Del("Transfer-Encoding") + header.Del("Upgrade") +} + +func removeExtraHTTPHostPort(req *http.Request) { + host := req.Host + if host == "" { + host = req.URL.Host + } + if pHost, port, err := net.SplitHostPort(host); err == nil && port == "80" { + host = pHost + } + req.Host = host + req.URL.Host = host +} + +// sendSimpleResponse sends a simple HTTP response with the given status code. +func sendSimpleResponse(conn net.Conn, req *http.Request, statusCode int) error { + resp := &http.Response{ + StatusCode: statusCode, + Status: http.StatusText(statusCode), + Proto: req.Proto, + ProtoMajor: req.ProtoMajor, + ProtoMinor: req.ProtoMinor, + Header: http.Header{}, + } + // Remove the "Content-Length: 0" header, some clients (e.g. ffmpeg) may not like it. + resp.ContentLength = -1 + // Also, prevent the "Connection: close" header. + resp.Close = false + resp.Uncompressed = true + return resp.Write(conn) +} + +// sendProxyAuthRequired sends a 407 Proxy Authentication Required response. +func sendProxyAuthRequired(conn net.Conn, req *http.Request, realm string) error { + resp := &http.Response{ + StatusCode: http.StatusProxyAuthRequired, + Status: http.StatusText(http.StatusProxyAuthRequired), + Proto: req.Proto, + ProtoMajor: req.ProtoMajor, + ProtoMinor: req.ProtoMinor, + Header: http.Header{}, + } + resp.Header.Set("Proxy-Authenticate", fmt.Sprintf("Basic realm=%q", realm)) + return resp.Write(conn) +} diff --git a/v2rayng/hysteria/app/internal/http/server_test.go b/v2rayng/hysteria/app/internal/http/server_test.go new file mode 100644 index 0000000000..43f6e16b71 --- /dev/null +++ b/v2rayng/hysteria/app/internal/http/server_test.go @@ -0,0 +1,59 @@ +package http + +import ( + "errors" + "net" + "net/http" + "os/exec" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/apernet/hysteria/core/v2/client" +) + +const ( + testCertFile = "test.crt" + testKeyFile = "test.key" +) + +type mockHyClient struct{} + +func (c *mockHyClient) TCP(addr string) (net.Conn, error) { + return net.Dial("tcp", addr) +} + +func (c *mockHyClient) UDP() (client.HyUDPConn, error) { + return nil, errors.New("not implemented") +} + +func (c *mockHyClient) Close() error { + return nil +} + +func TestServer(t *testing.T) { + // Start the server + l, err := net.Listen("tcp", "127.0.0.1:18080") + assert.NoError(t, err) + defer l.Close() + s := &Server{ + HyClient: &mockHyClient{}, + } + go s.Serve(l) + + // Start a test HTTP & HTTPS server + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("control is an illusion")) + }) + go http.ListenAndServe("127.0.0.1:18081", nil) + go http.ListenAndServeTLS("127.0.0.1:18082", testCertFile, testKeyFile, nil) + + // Run the Python test script + cmd := exec.Command("python", "server_test.py") + // Suppress HTTPS warning text from Python + cmd.Env = append(cmd.Env, "PYTHONWARNINGS=ignore:Unverified HTTPS request") + out, err := cmd.CombinedOutput() + assert.NoError(t, err) + assert.Equal(t, "OK", strings.TrimSpace(string(out))) +} diff --git a/v2rayng/hysteria/app/internal/http/server_test.py b/v2rayng/hysteria/app/internal/http/server_test.py new file mode 100644 index 0000000000..46c638fcca --- /dev/null +++ b/v2rayng/hysteria/app/internal/http/server_test.py @@ -0,0 +1,24 @@ +import requests + +proxies = { + "http": "http://127.0.0.1:18080", + "https": "http://127.0.0.1:18080", +} + + +def test_http(it): + for i in range(it): + r = requests.get("http://127.0.0.1:18081", proxies=proxies) + assert r.status_code == 200 and r.text == "control is an illusion" + + +def test_https(it): + for i in range(it): + r = requests.get("https://127.0.0.1:18082", proxies=proxies, verify=False) + assert r.status_code == 200 and r.text == "control is an illusion" + + +if __name__ == "__main__": + test_http(10) + test_https(10) + print("OK") diff --git a/v2rayng/hysteria/app/internal/http/test.crt b/v2rayng/hysteria/app/internal/http/test.crt new file mode 100644 index 0000000000..ecb00ed17e --- /dev/null +++ b/v2rayng/hysteria/app/internal/http/test.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDwTCCAqmgAwIBAgIUMeefneiCXWS2ovxNN+fJcdrOIfAwDQYJKoZIhvcNAQEL +BQAwcDELMAkGA1UEBhMCVFcxEzARBgNVBAgMClNvbWUtU3RhdGUxGTAXBgNVBAoM +EFJhbmRvbSBTdHVmZiBMTEMxEjAQBgNVBAMMCWxvY2FsaG9zdDEdMBsGCSqGSIb3 +DQEJARYOcG9vcGVyQHNoaXQuY2MwHhcNMjMwNDI3MDAyMDQ1WhcNMzMwNDI0MDAy +MDQ1WjBwMQswCQYDVQQGEwJUVzETMBEGA1UECAwKU29tZS1TdGF0ZTEZMBcGA1UE +CgwQUmFuZG9tIFN0dWZmIExMQzESMBAGA1UEAwwJbG9jYWxob3N0MR0wGwYJKoZI +hvcNAQkBFg5wb29wZXJAc2hpdC5jYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAOU9/4AT/6fDKyEyZMMLFzUEVC8ZDJHoKZ+3g65ZFQLxRKqlEdhvOwq4 +ZsxYF0sceUPDAsdrT+km0l1jAvq6u82n6xQQ60HpKe6hOvDX7KS0dPcKa+nfEa0W +DKamBB+TzxB2dBfBNS1oUU74nBb7ttpJiKnOpRJ0/J+CwslvhJzq04AUXC/W1CtW +CbZBg1JjY0fCN+Oy1WjEqMtRSB6k5Ipk40a8NcsqReBOMZChR8elruZ09sIlA6tf +jICOKToDVBmkjJ8m/GnxfV8MeLoK83M2VA73njsS6q9qe9KDVgIVQmifwi6JUb7N +o0A6f2Z47AWJmvq4goHJtnQ3fyoeIsMCAwEAAaNTMFEwHQYDVR0OBBYEFPrBsm6v +M29fKA3is22tK8yHYQaDMB8GA1UdIwQYMBaAFPrBsm6vM29fKA3is22tK8yHYQaD +MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJvOwj0Tf8l9AWvf +1ZLyW0K3m5oJAoUayjlLP9q7KHgJHWd4QXxg4ApUDo523m4Own3FwtN06KCMqlxc +luDJi27ghRzZ8bpB9fUujikC1rs1oWYRz/K+JSO1VItan+azm9AQRj+nNepjUiT4 +FjvRif+inC4392tcKuwrqiUFmLIggtFZdsLeKUL+hRGCRjY4BZw0d1sjjPtyVNUD +UMVO8pxlCV0NU4Nmt3vulD4YshAXM+Y8yX/vPRnaNGoRrbRgCg2VORRGaZVjQMHD +OLMvqM7pFKnVg0uiSbQ3xbQJ8WeX620zKI0So2+kZt9HoI+46gd7BdNfl7mmd6K7 +ydYKuI8= +-----END CERTIFICATE----- diff --git a/v2rayng/hysteria/app/internal/http/test.key b/v2rayng/hysteria/app/internal/http/test.key new file mode 100644 index 0000000000..d471f50de5 --- /dev/null +++ b/v2rayng/hysteria/app/internal/http/test.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA5T3/gBP/p8MrITJkwwsXNQRULxkMkegpn7eDrlkVAvFEqqUR +2G87CrhmzFgXSxx5Q8MCx2tP6SbSXWMC+rq7zafrFBDrQekp7qE68NfspLR09wpr +6d8RrRYMpqYEH5PPEHZ0F8E1LWhRTvicFvu22kmIqc6lEnT8n4LCyW+EnOrTgBRc +L9bUK1YJtkGDUmNjR8I347LVaMSoy1FIHqTkimTjRrw1yypF4E4xkKFHx6Wu5nT2 +wiUDq1+MgI4pOgNUGaSMnyb8afF9Xwx4ugrzczZUDveeOxLqr2p70oNWAhVCaJ/C +LolRvs2jQDp/ZnjsBYma+riCgcm2dDd/Kh4iwwIDAQABAoIBABjiU/vJL/U8AFCI +MdviNlCw+ZprM6wa8Xm+5/JjBR7epb+IT5mY6WXOgoon/c9PdfJfFswi3/fFGQy+ +FLK21nAKjEAPXho3fy/CHK3MIon2dMPkQ7aNWlPZkuH8H3J2DwIQeaWieW1GZ50U +64yrIjwrw0P7hHuua0W9YfuPuWt29YpW5g6ilSRE0kdTzoB6TgMzlVRj6RWbxWLX +erwYFesSpLPiQrozK2yywlQsvRV2AxTlf5woJyRTyCqcao5jNZOJJl0mqeGKNKbu +1iYGtZl9aj1XIRxUt+JB2IMKNJasygIp+GRLUDCHKh8RVFwRlVaSNcWbfLDuyNWW +T3lUEjECgYEA84mrs4TLuPfklsQM4WPBdN/2Ud1r0Zn/W8icHcVc/DCFXbcV4aPA +g4yyyyEkyTac2RSbSp+rfUk/pJcG6CVjwaiRIPehdtcLIUP34EdIrwPrPT7/uWVA +o/Hp1ANSILecknQXeE1qDlHVeGAq2k3vAQH2J0m7lMfar7QCBTMTMHcCgYEA8PkO +Uj9+/LoHod2eb4raH29wntis31X5FX/C/8HlmFmQplxfMxpRckzDYQELdHvDggNY +ZQo6pdE22MjCu2bk9AHa2ukMyieWm/mPe46Upr1YV2o5cWnfFFNa/LP2Ii/dWY5V +rFNsHFnrnwcWymX7OKo0Xb8xYnKhKZJAFwSpXxUCgYBPMjXj6wtU20g6vwZxRT9k +AnDXrmmhf7LK5jHefJAAcsbr8t3qwpWYMejypZSQ2nGnJkxZuBLMa0WHAJX+aCpI +j8iiL+USAFxeNPwmswev4lZdVF9Uqtiad9DSYUIT4aHI/nejZ4lVnscMnjlRRIa0 +jS6/F/soJtW2zZLangFfgQKBgCOSAAUwDkSsCThhiGOasXv2bT9laI9HF4+O3m/2 +ZTfJ8Mo91GesuN0Qa77D8rbtFfz5FXFEw0d6zIfPir8y/xTtuSqbQCIPGfJIMl/g +uhyq0oGE0pnlMOLFMyceQXTmb9wqYIchgVHmDBvbZgfWafEBXt1/vYB0v0ltpzw+ +menJAoGBAI0hx3+mrFgA+xJBEk4oexAlro1qbNWoR7BCmLQtd49jG3eZQu4JxWH2 +kh58AIXzLl0X9t4pfMYasYL6jBGvw+AqNdo2krpiL7MWEE8w8FP/wibzqmuloziB +T7BZuCZjpcAM0IxLmQeeUK0LF0mihcqvssxveaet46mj7QoA7bGQ +-----END RSA PRIVATE KEY----- diff --git a/v2rayng/hysteria/app/internal/proxymux/.mockery.yaml b/v2rayng/hysteria/app/internal/proxymux/.mockery.yaml new file mode 100644 index 0000000000..7d3fac08a5 --- /dev/null +++ b/v2rayng/hysteria/app/internal/proxymux/.mockery.yaml @@ -0,0 +1,12 @@ +with-expecter: true +dir: internal/mocks +outpkg: mocks +packages: + net: + interfaces: + Listener: + config: + mockname: MockListener + Conn: + config: + mockname: MockConn diff --git a/v2rayng/hysteria/app/internal/proxymux/internal/mocks/mock_Conn.go b/v2rayng/hysteria/app/internal/proxymux/internal/mocks/mock_Conn.go new file mode 100644 index 0000000000..13e363e999 --- /dev/null +++ b/v2rayng/hysteria/app/internal/proxymux/internal/mocks/mock_Conn.go @@ -0,0 +1,427 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package mocks + +import ( + net "net" + + mock "github.com/stretchr/testify/mock" + + time "time" +) + +// MockConn is an autogenerated mock type for the Conn type +type MockConn struct { + mock.Mock +} + +type MockConn_Expecter struct { + mock *mock.Mock +} + +func (_m *MockConn) EXPECT() *MockConn_Expecter { + return &MockConn_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function with given fields: +func (_m *MockConn) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type MockConn_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *MockConn_Expecter) Close() *MockConn_Close_Call { + return &MockConn_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *MockConn_Close_Call) Run(run func()) *MockConn_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockConn_Close_Call) Return(_a0 error) *MockConn_Close_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConn_Close_Call) RunAndReturn(run func() error) *MockConn_Close_Call { + _c.Call.Return(run) + return _c +} + +// LocalAddr provides a mock function with given fields: +func (_m *MockConn) LocalAddr() net.Addr { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for LocalAddr") + } + + var r0 net.Addr + if rf, ok := ret.Get(0).(func() net.Addr); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(net.Addr) + } + } + + return r0 +} + +// MockConn_LocalAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LocalAddr' +type MockConn_LocalAddr_Call struct { + *mock.Call +} + +// LocalAddr is a helper method to define mock.On call +func (_e *MockConn_Expecter) LocalAddr() *MockConn_LocalAddr_Call { + return &MockConn_LocalAddr_Call{Call: _e.mock.On("LocalAddr")} +} + +func (_c *MockConn_LocalAddr_Call) Run(run func()) *MockConn_LocalAddr_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockConn_LocalAddr_Call) Return(_a0 net.Addr) *MockConn_LocalAddr_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConn_LocalAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_LocalAddr_Call { + _c.Call.Return(run) + return _c +} + +// Read provides a mock function with given fields: b +func (_m *MockConn) Read(b []byte) (int, error) { + ret := _m.Called(b) + + if len(ret) == 0 { + panic("no return value specified for Read") + } + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok { + return rf(b) + } + if rf, ok := ret.Get(0).(func([]byte) int); ok { + r0 = rf(b) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(b) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockConn_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read' +type MockConn_Read_Call struct { + *mock.Call +} + +// Read is a helper method to define mock.On call +// - b []byte +func (_e *MockConn_Expecter) Read(b interface{}) *MockConn_Read_Call { + return &MockConn_Read_Call{Call: _e.mock.On("Read", b)} +} + +func (_c *MockConn_Read_Call) Run(run func(b []byte)) *MockConn_Read_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte)) + }) + return _c +} + +func (_c *MockConn_Read_Call) Return(n int, err error) *MockConn_Read_Call { + _c.Call.Return(n, err) + return _c +} + +func (_c *MockConn_Read_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Read_Call { + _c.Call.Return(run) + return _c +} + +// RemoteAddr provides a mock function with given fields: +func (_m *MockConn) RemoteAddr() net.Addr { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for RemoteAddr") + } + + var r0 net.Addr + if rf, ok := ret.Get(0).(func() net.Addr); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(net.Addr) + } + } + + return r0 +} + +// MockConn_RemoteAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoteAddr' +type MockConn_RemoteAddr_Call struct { + *mock.Call +} + +// RemoteAddr is a helper method to define mock.On call +func (_e *MockConn_Expecter) RemoteAddr() *MockConn_RemoteAddr_Call { + return &MockConn_RemoteAddr_Call{Call: _e.mock.On("RemoteAddr")} +} + +func (_c *MockConn_RemoteAddr_Call) Run(run func()) *MockConn_RemoteAddr_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockConn_RemoteAddr_Call) Return(_a0 net.Addr) *MockConn_RemoteAddr_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConn_RemoteAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_RemoteAddr_Call { + _c.Call.Return(run) + return _c +} + +// SetDeadline provides a mock function with given fields: t +func (_m *MockConn) SetDeadline(t time.Time) error { + ret := _m.Called(t) + + if len(ret) == 0 { + panic("no return value specified for SetDeadline") + } + + var r0 error + if rf, ok := ret.Get(0).(func(time.Time) error); ok { + r0 = rf(t) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockConn_SetDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDeadline' +type MockConn_SetDeadline_Call struct { + *mock.Call +} + +// SetDeadline is a helper method to define mock.On call +// - t time.Time +func (_e *MockConn_Expecter) SetDeadline(t interface{}) *MockConn_SetDeadline_Call { + return &MockConn_SetDeadline_Call{Call: _e.mock.On("SetDeadline", t)} +} + +func (_c *MockConn_SetDeadline_Call) Run(run func(t time.Time)) *MockConn_SetDeadline_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Time)) + }) + return _c +} + +func (_c *MockConn_SetDeadline_Call) Return(_a0 error) *MockConn_SetDeadline_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConn_SetDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetDeadline_Call { + _c.Call.Return(run) + return _c +} + +// SetReadDeadline provides a mock function with given fields: t +func (_m *MockConn) SetReadDeadline(t time.Time) error { + ret := _m.Called(t) + + if len(ret) == 0 { + panic("no return value specified for SetReadDeadline") + } + + var r0 error + if rf, ok := ret.Get(0).(func(time.Time) error); ok { + r0 = rf(t) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockConn_SetReadDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetReadDeadline' +type MockConn_SetReadDeadline_Call struct { + *mock.Call +} + +// SetReadDeadline is a helper method to define mock.On call +// - t time.Time +func (_e *MockConn_Expecter) SetReadDeadline(t interface{}) *MockConn_SetReadDeadline_Call { + return &MockConn_SetReadDeadline_Call{Call: _e.mock.On("SetReadDeadline", t)} +} + +func (_c *MockConn_SetReadDeadline_Call) Run(run func(t time.Time)) *MockConn_SetReadDeadline_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Time)) + }) + return _c +} + +func (_c *MockConn_SetReadDeadline_Call) Return(_a0 error) *MockConn_SetReadDeadline_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConn_SetReadDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetReadDeadline_Call { + _c.Call.Return(run) + return _c +} + +// SetWriteDeadline provides a mock function with given fields: t +func (_m *MockConn) SetWriteDeadline(t time.Time) error { + ret := _m.Called(t) + + if len(ret) == 0 { + panic("no return value specified for SetWriteDeadline") + } + + var r0 error + if rf, ok := ret.Get(0).(func(time.Time) error); ok { + r0 = rf(t) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockConn_SetWriteDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWriteDeadline' +type MockConn_SetWriteDeadline_Call struct { + *mock.Call +} + +// SetWriteDeadline is a helper method to define mock.On call +// - t time.Time +func (_e *MockConn_Expecter) SetWriteDeadline(t interface{}) *MockConn_SetWriteDeadline_Call { + return &MockConn_SetWriteDeadline_Call{Call: _e.mock.On("SetWriteDeadline", t)} +} + +func (_c *MockConn_SetWriteDeadline_Call) Run(run func(t time.Time)) *MockConn_SetWriteDeadline_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Time)) + }) + return _c +} + +func (_c *MockConn_SetWriteDeadline_Call) Return(_a0 error) *MockConn_SetWriteDeadline_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConn_SetWriteDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetWriteDeadline_Call { + _c.Call.Return(run) + return _c +} + +// Write provides a mock function with given fields: b +func (_m *MockConn) Write(b []byte) (int, error) { + ret := _m.Called(b) + + if len(ret) == 0 { + panic("no return value specified for Write") + } + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok { + return rf(b) + } + if rf, ok := ret.Get(0).(func([]byte) int); ok { + r0 = rf(b) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(b) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockConn_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write' +type MockConn_Write_Call struct { + *mock.Call +} + +// Write is a helper method to define mock.On call +// - b []byte +func (_e *MockConn_Expecter) Write(b interface{}) *MockConn_Write_Call { + return &MockConn_Write_Call{Call: _e.mock.On("Write", b)} +} + +func (_c *MockConn_Write_Call) Run(run func(b []byte)) *MockConn_Write_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte)) + }) + return _c +} + +func (_c *MockConn_Write_Call) Return(n int, err error) *MockConn_Write_Call { + _c.Call.Return(n, err) + return _c +} + +func (_c *MockConn_Write_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Write_Call { + _c.Call.Return(run) + return _c +} + +// NewMockConn creates a new instance of MockConn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockConn(t interface { + mock.TestingT + Cleanup(func()) +}) *MockConn { + mock := &MockConn{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/v2rayng/hysteria/app/internal/proxymux/internal/mocks/mock_Listener.go b/v2rayng/hysteria/app/internal/proxymux/internal/mocks/mock_Listener.go new file mode 100644 index 0000000000..842b88f2d8 --- /dev/null +++ b/v2rayng/hysteria/app/internal/proxymux/internal/mocks/mock_Listener.go @@ -0,0 +1,185 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package mocks + +import ( + net "net" + + mock "github.com/stretchr/testify/mock" +) + +// MockListener is an autogenerated mock type for the Listener type +type MockListener struct { + mock.Mock +} + +type MockListener_Expecter struct { + mock *mock.Mock +} + +func (_m *MockListener) EXPECT() *MockListener_Expecter { + return &MockListener_Expecter{mock: &_m.Mock} +} + +// Accept provides a mock function with given fields: +func (_m *MockListener) Accept() (net.Conn, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Accept") + } + + var r0 net.Conn + var r1 error + if rf, ok := ret.Get(0).(func() (net.Conn, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() net.Conn); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(net.Conn) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockListener_Accept_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Accept' +type MockListener_Accept_Call struct { + *mock.Call +} + +// Accept is a helper method to define mock.On call +func (_e *MockListener_Expecter) Accept() *MockListener_Accept_Call { + return &MockListener_Accept_Call{Call: _e.mock.On("Accept")} +} + +func (_c *MockListener_Accept_Call) Run(run func()) *MockListener_Accept_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockListener_Accept_Call) Return(_a0 net.Conn, _a1 error) *MockListener_Accept_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockListener_Accept_Call) RunAndReturn(run func() (net.Conn, error)) *MockListener_Accept_Call { + _c.Call.Return(run) + return _c +} + +// Addr provides a mock function with given fields: +func (_m *MockListener) Addr() net.Addr { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Addr") + } + + var r0 net.Addr + if rf, ok := ret.Get(0).(func() net.Addr); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(net.Addr) + } + } + + return r0 +} + +// MockListener_Addr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Addr' +type MockListener_Addr_Call struct { + *mock.Call +} + +// Addr is a helper method to define mock.On call +func (_e *MockListener_Expecter) Addr() *MockListener_Addr_Call { + return &MockListener_Addr_Call{Call: _e.mock.On("Addr")} +} + +func (_c *MockListener_Addr_Call) Run(run func()) *MockListener_Addr_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockListener_Addr_Call) Return(_a0 net.Addr) *MockListener_Addr_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockListener_Addr_Call) RunAndReturn(run func() net.Addr) *MockListener_Addr_Call { + _c.Call.Return(run) + return _c +} + +// Close provides a mock function with given fields: +func (_m *MockListener) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockListener_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type MockListener_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *MockListener_Expecter) Close() *MockListener_Close_Call { + return &MockListener_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *MockListener_Close_Call) Run(run func()) *MockListener_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockListener_Close_Call) Return(_a0 error) *MockListener_Close_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockListener_Close_Call) RunAndReturn(run func() error) *MockListener_Close_Call { + _c.Call.Return(run) + return _c +} + +// NewMockListener creates a new instance of MockListener. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockListener(t interface { + mock.TestingT + Cleanup(func()) +}) *MockListener { + mock := &MockListener{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/v2rayng/hysteria/app/internal/proxymux/manager.go b/v2rayng/hysteria/app/internal/proxymux/manager.go new file mode 100644 index 0000000000..23f4ad67ae --- /dev/null +++ b/v2rayng/hysteria/app/internal/proxymux/manager.go @@ -0,0 +1,72 @@ +package proxymux + +import ( + "net" + "sync" + + "github.com/apernet/hysteria/extras/v2/correctnet" +) + +type muxManager struct { + listeners map[string]*muxListener + lock sync.Mutex +} + +var globalMuxManager *muxManager + +func init() { + globalMuxManager = &muxManager{ + listeners: make(map[string]*muxListener), + } +} + +func (m *muxManager) GetOrCreate(address string) (*muxListener, error) { + key, err := m.canonicalizeAddrPort(address) + if err != nil { + return nil, err + } + + m.lock.Lock() + defer m.lock.Unlock() + + if ml, ok := m.listeners[key]; ok { + return ml, nil + } + + listener, err := correctnet.Listen("tcp", key) + if err != nil { + return nil, err + } + + ml := newMuxListener(listener, func() { + m.lock.Lock() + defer m.lock.Unlock() + delete(m.listeners, key) + }) + m.listeners[key] = ml + return ml, nil +} + +func (m *muxManager) canonicalizeAddrPort(address string) (string, error) { + taddr, err := net.ResolveTCPAddr("tcp", address) + if err != nil { + return "", err + } + return taddr.String(), nil +} + +func ListenHTTP(address string) (net.Listener, error) { + ml, err := globalMuxManager.GetOrCreate(address) + if err != nil { + return nil, err + } + return ml.ListenHTTP() +} + +func ListenSOCKS(address string) (net.Listener, error) { + ml, err := globalMuxManager.GetOrCreate(address) + if err != nil { + return nil, err + } + return ml.ListenSOCKS() +} diff --git a/v2rayng/hysteria/app/internal/proxymux/manager_test.go b/v2rayng/hysteria/app/internal/proxymux/manager_test.go new file mode 100644 index 0000000000..c776058f1e --- /dev/null +++ b/v2rayng/hysteria/app/internal/proxymux/manager_test.go @@ -0,0 +1,110 @@ +package proxymux + +import ( + "net" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestListenSOCKS(t *testing.T) { + address := "127.2.39.129:11081" + + sl, err := ListenSOCKS(address) + if !assert.NoError(t, err) { + return + } + defer func() { + sl.Close() + }() + + hl, err := ListenHTTP(address) + if !assert.NoError(t, err) { + return + } + defer hl.Close() + + _, err = ListenSOCKS(address) + if !assert.ErrorIs(t, err, ErrProtocolInUse) { + return + } + sl.Close() + + sl, err = ListenSOCKS(address) + if !assert.NoError(t, err) { + return + } +} + +func TestListenHTTP(t *testing.T) { + address := "127.2.39.129:11082" + + hl, err := ListenHTTP(address) + if !assert.NoError(t, err) { + return + } + defer func() { + hl.Close() + }() + + sl, err := ListenSOCKS(address) + if !assert.NoError(t, err) { + return + } + defer sl.Close() + + _, err = ListenHTTP(address) + if !assert.ErrorIs(t, err, ErrProtocolInUse) { + return + } + hl.Close() + + hl, err = ListenHTTP(address) + if !assert.NoError(t, err) { + return + } +} + +func TestRelease(t *testing.T) { + address := "127.2.39.129:11083" + + hl, err := ListenHTTP(address) + if !assert.NoError(t, err) { + return + } + sl, err := ListenSOCKS(address) + if !assert.NoError(t, err) { + return + } + + if !assert.True(t, globalMuxManager.testAddressExists(address)) { + return + } + _, err = net.Listen("tcp", address) + if !assert.Error(t, err) { + return + } + + hl.Close() + sl.Close() + + // Wait for muxListener released + time.Sleep(time.Second) + if !assert.False(t, globalMuxManager.testAddressExists(address)) { + return + } + lis, err := net.Listen("tcp", address) + if !assert.NoError(t, err) { + return + } + defer lis.Close() +} + +func (m *muxManager) testAddressExists(address string) bool { + m.lock.Lock() + defer m.lock.Unlock() + + _, ok := m.listeners[address] + return ok +} diff --git a/v2rayng/hysteria/app/internal/proxymux/mux.go b/v2rayng/hysteria/app/internal/proxymux/mux.go new file mode 100644 index 0000000000..1f0b7b09dd --- /dev/null +++ b/v2rayng/hysteria/app/internal/proxymux/mux.go @@ -0,0 +1,320 @@ +package proxymux + +import ( + "errors" + "fmt" + "io" + "net" + "sync" +) + +func newMuxListener(listener net.Listener, deleteFunc func()) *muxListener { + l := &muxListener{ + base: listener, + acceptChan: make(chan net.Conn), + closeChan: make(chan struct{}), + deleteFunc: deleteFunc, + } + go l.acceptLoop() + go l.mainLoop() + return l +} + +type muxListener struct { + lock sync.Mutex + base net.Listener + acceptErr error + + acceptChan chan net.Conn + closeChan chan struct{} + + socksListener *subListener + httpListener *subListener + + deleteFunc func() +} + +func (l *muxListener) acceptLoop() { + defer close(l.acceptChan) + + for { + conn, err := l.base.Accept() + if err != nil { + l.lock.Lock() + l.acceptErr = err + l.lock.Unlock() + return + } + select { + case <-l.closeChan: + return + case l.acceptChan <- conn: + } + } +} + +func (l *muxListener) mainLoop() { + defer func() { + l.deleteFunc() + l.base.Close() + + close(l.closeChan) + + l.lock.Lock() + defer l.lock.Unlock() + + if sl := l.httpListener; sl != nil { + close(sl.acceptChan) + l.httpListener = nil + } + if sl := l.socksListener; sl != nil { + close(sl.acceptChan) + l.socksListener = nil + } + }() + + for { + var socksCloseChan, httpCloseChan chan struct{} + if l.httpListener != nil { + httpCloseChan = l.httpListener.closeChan + } + if l.socksListener != nil { + socksCloseChan = l.socksListener.closeChan + } + select { + case <-l.closeChan: + return + case conn, ok := <-l.acceptChan: + if !ok { + return + } + go l.dispatch(conn) + case <-socksCloseChan: + l.lock.Lock() + if socksCloseChan == l.socksListener.closeChan { + // not replaced by another ListenSOCKS() + l.socksListener = nil + } + l.lock.Unlock() + if l.checkIdle() { + return + } + case <-httpCloseChan: + l.lock.Lock() + if httpCloseChan == l.httpListener.closeChan { + // not replaced by another ListenHTTP() + l.httpListener = nil + } + l.lock.Unlock() + if l.checkIdle() { + return + } + } + } +} + +func (l *muxListener) dispatch(conn net.Conn) { + var b [1]byte + if _, err := io.ReadFull(conn, b[:]); err != nil { + conn.Close() + return + } + + l.lock.Lock() + var target *subListener + if b[0] == 5 { + target = l.socksListener + } else { + target = l.httpListener + } + l.lock.Unlock() + + if target == nil { + conn.Close() + return + } + + wconn := &connWithOneByte{Conn: conn, b: b[0]} + + select { + case <-target.closeChan: + case target.acceptChan <- wconn: + } +} + +func (l *muxListener) checkIdle() bool { + l.lock.Lock() + defer l.lock.Unlock() + + return l.httpListener == nil && l.socksListener == nil +} + +func (l *muxListener) getAndClearAcceptError() error { + l.lock.Lock() + defer l.lock.Unlock() + + if l.acceptErr == nil { + return nil + } + err := l.acceptErr + l.acceptErr = nil + return err +} + +func (l *muxListener) ListenHTTP() (net.Listener, error) { + l.lock.Lock() + defer l.lock.Unlock() + + if l.httpListener != nil { + subListenerPendingClosed := false + select { + case <-l.httpListener.closeChan: + subListenerPendingClosed = true + default: + } + if !subListenerPendingClosed { + return nil, OpErr{ + Addr: l.base.Addr(), + Protocol: "http", + Op: "bind-protocol", + Err: ErrProtocolInUse, + } + } + l.httpListener = nil + } + + select { + case <-l.closeChan: + return nil, net.ErrClosed + default: + } + + sl := newSubListener(l.getAndClearAcceptError, l.base.Addr) + l.httpListener = sl + return sl, nil +} + +func (l *muxListener) ListenSOCKS() (net.Listener, error) { + l.lock.Lock() + defer l.lock.Unlock() + + if l.socksListener != nil { + subListenerPendingClosed := false + select { + case <-l.socksListener.closeChan: + subListenerPendingClosed = true + default: + } + if !subListenerPendingClosed { + return nil, OpErr{ + Addr: l.base.Addr(), + Protocol: "socks", + Op: "bind-protocol", + Err: ErrProtocolInUse, + } + } + l.socksListener = nil + } + + select { + case <-l.closeChan: + return nil, net.ErrClosed + default: + } + + sl := newSubListener(l.getAndClearAcceptError, l.base.Addr) + l.socksListener = sl + return sl, nil +} + +func newSubListener(acceptErrorFunc func() error, addrFunc func() net.Addr) *subListener { + return &subListener{ + acceptChan: make(chan net.Conn), + acceptErrorFunc: acceptErrorFunc, + closeChan: make(chan struct{}), + addrFunc: addrFunc, + } +} + +type subListener struct { + // receive connections or closure from upstream + acceptChan chan net.Conn + // get an error of Accept() from upstream + acceptErrorFunc func() error + // notify upstream that we are closed + closeChan chan struct{} + + // Listener.Addr() implementation of base listener + addrFunc func() net.Addr +} + +func (l *subListener) Accept() (net.Conn, error) { + select { + case <-l.closeChan: + // closed by ourselves + return nil, net.ErrClosed + case conn, ok := <-l.acceptChan: + if !ok { + // closed by upstream + if acceptErr := l.acceptErrorFunc(); acceptErr != nil { + return nil, acceptErr + } + return nil, net.ErrClosed + } + return conn, nil + } +} + +func (l *subListener) Addr() net.Addr { + return l.addrFunc() +} + +// Close implements net.Listener.Close. +// Upstream should use close(l.acceptChan) instead. +func (l *subListener) Close() error { + select { + case <-l.closeChan: + return nil + default: + } + close(l.closeChan) + return nil +} + +// connWithOneByte is a net.Conn that returns b for the first read +// request, then forwards everything else to Conn. +type connWithOneByte struct { + net.Conn + + b byte + bRead bool +} + +func (c *connWithOneByte) Read(bs []byte) (int, error) { + if c.bRead { + return c.Conn.Read(bs) + } + if len(bs) == 0 { + return 0, nil + } + c.bRead = true + bs[0] = c.b + return 1, nil +} + +type OpErr struct { + Addr net.Addr + Protocol string + Op string + Err error +} + +func (m OpErr) Error() string { + return fmt.Sprintf("mux-listen: %s[%s]: %s: %v", m.Addr, m.Protocol, m.Op, m.Err) +} + +func (m OpErr) Unwrap() error { + return m.Err +} + +var ErrProtocolInUse = errors.New("protocol already in use") diff --git a/v2rayng/hysteria/app/internal/proxymux/mux_test.go b/v2rayng/hysteria/app/internal/proxymux/mux_test.go new file mode 100644 index 0000000000..7b57237931 --- /dev/null +++ b/v2rayng/hysteria/app/internal/proxymux/mux_test.go @@ -0,0 +1,154 @@ +package proxymux + +import ( + "bytes" + "net" + "sync" + "testing" + "time" + + "github.com/apernet/hysteria/app/v2/internal/proxymux/internal/mocks" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +//go:generate mockery + +func testMockListener(t *testing.T, connChan <-chan net.Conn) net.Listener { + closedChan := make(chan struct{}) + mockListener := mocks.NewMockListener(t) + mockListener.EXPECT().Accept().RunAndReturn(func() (net.Conn, error) { + select { + case <-closedChan: + return nil, net.ErrClosed + case conn, ok := <-connChan: + if !ok { + panic("unexpected closed channel (connChan)") + } + return conn, nil + } + }) + mockListener.EXPECT().Close().RunAndReturn(func() error { + select { + case <-closedChan: + default: + close(closedChan) + } + return nil + }) + return mockListener +} + +func testMockConn(t *testing.T, b []byte) net.Conn { + buf := bytes.NewReader(b) + isClosed := false + mockConn := mocks.NewMockConn(t) + mockConn.EXPECT().Read(mock.Anything).RunAndReturn(func(b []byte) (int, error) { + if isClosed { + return 0, net.ErrClosed + } + return buf.Read(b) + }) + mockConn.EXPECT().Close().RunAndReturn(func() error { + isClosed = true + return nil + }) + return mockConn +} + +func TestMuxHTTP(t *testing.T) { + connChan := make(chan net.Conn) + mockListener := testMockListener(t, connChan) + mockConn := testMockConn(t, []byte("CONNECT example.com:443 HTTP/1.1\r\n\r\n")) + + mux := newMuxListener(mockListener, func() {}) + hl, err := mux.ListenHTTP() + if !assert.NoError(t, err) { + return + } + sl, err := mux.ListenSOCKS() + if !assert.NoError(t, err) { + return + } + + connChan <- mockConn + + var socksConn, httpConn net.Conn + var socksErr, httpErr error + + var wg sync.WaitGroup + wg.Add(2) + go func() { + socksConn, socksErr = sl.Accept() + wg.Done() + }() + go func() { + httpConn, httpErr = hl.Accept() + wg.Done() + }() + + time.Sleep(time.Second) + + sl.Close() + hl.Close() + + wg.Wait() + + assert.Nil(t, socksConn) + assert.ErrorIs(t, socksErr, net.ErrClosed) + assert.NotNil(t, httpConn) + httpConn.Close() + assert.NoError(t, httpErr) + + // Wait for muxListener released + <-mux.acceptChan +} + +func TestMuxSOCKS(t *testing.T) { + connChan := make(chan net.Conn) + mockListener := testMockListener(t, connChan) + mockConn := testMockConn(t, []byte{0x05, 0x02, 0x00, 0x01}) // SOCKS5 Connect Request: NOAUTH+GSSAPI + + mux := newMuxListener(mockListener, func() {}) + hl, err := mux.ListenHTTP() + if !assert.NoError(t, err) { + return + } + sl, err := mux.ListenSOCKS() + if !assert.NoError(t, err) { + return + } + + connChan <- mockConn + + var socksConn, httpConn net.Conn + var socksErr, httpErr error + + var wg sync.WaitGroup + wg.Add(2) + go func() { + socksConn, socksErr = sl.Accept() + wg.Done() + }() + go func() { + httpConn, httpErr = hl.Accept() + wg.Done() + }() + + time.Sleep(time.Second) + + sl.Close() + hl.Close() + + wg.Wait() + + assert.NotNil(t, socksConn) + socksConn.Close() + assert.NoError(t, socksErr) + assert.Nil(t, httpConn) + assert.ErrorIs(t, httpErr, net.ErrClosed) + + // Wait for muxListener released + <-mux.acceptChan +} diff --git a/v2rayng/hysteria/app/internal/redirect/getsockopt_linux.go b/v2rayng/hysteria/app/internal/redirect/getsockopt_linux.go new file mode 100644 index 0000000000..b84593e0c2 --- /dev/null +++ b/v2rayng/hysteria/app/internal/redirect/getsockopt_linux.go @@ -0,0 +1,17 @@ +//go:build !386 +// +build !386 + +package redirect + +import ( + "syscall" + "unsafe" +) + +func getsockopt(s, level, name uintptr, val unsafe.Pointer, vallen *uint32) (err error) { + _, _, e := syscall.Syscall6(syscall.SYS_GETSOCKOPT, s, level, name, uintptr(val), uintptr(unsafe.Pointer(vallen)), 0) + if e != 0 { + err = e + } + return +} diff --git a/v2rayng/hysteria/app/internal/redirect/getsockopt_linux_386.go b/v2rayng/hysteria/app/internal/redirect/getsockopt_linux_386.go new file mode 100644 index 0000000000..f3832ec781 --- /dev/null +++ b/v2rayng/hysteria/app/internal/redirect/getsockopt_linux_386.go @@ -0,0 +1,23 @@ +package redirect + +import ( + "syscall" + "unsafe" +) + +const ( + sysGetsockopt = 15 +) + +// On 386 we cannot call socketcall with syscall.Syscall6, as it always fails with EFAULT. +// Use our own syscall.socketcall hack instead. + +func syscall_socketcall(call int, a0, a1, a2, a3, a4, a5 uintptr) (n int, err syscall.Errno) + +func getsockopt(s, level, name uintptr, val unsafe.Pointer, vallen *uint32) (err error) { + _, e := syscall_socketcall(sysGetsockopt, s, level, name, uintptr(val), uintptr(unsafe.Pointer(vallen)), 0) + if e != 0 { + err = e + } + return +} diff --git a/v2rayng/hysteria/app/internal/redirect/syscall_socketcall_linux_386.s b/v2rayng/hysteria/app/internal/redirect/syscall_socketcall_linux_386.s new file mode 100644 index 0000000000..2dab43ad03 --- /dev/null +++ b/v2rayng/hysteria/app/internal/redirect/syscall_socketcall_linux_386.s @@ -0,0 +1,7 @@ +//go:build gc +// +build gc + +#include "textflag.h" + +TEXT ·syscall_socketcall(SB),NOSPLIT,$0-36 + JMP syscall·socketcall(SB) diff --git a/v2rayng/hysteria/app/internal/redirect/tcp_linux.go b/v2rayng/hysteria/app/internal/redirect/tcp_linux.go new file mode 100644 index 0000000000..53515433f0 --- /dev/null +++ b/v2rayng/hysteria/app/internal/redirect/tcp_linux.go @@ -0,0 +1,126 @@ +package redirect + +import ( + "encoding/binary" + "errors" + "io" + "net" + "syscall" + "unsafe" + + "github.com/apernet/hysteria/core/v2/client" +) + +const ( + soOriginalDst = 80 + soOriginalDstV6 = 80 +) + +type TCPRedirect struct { + HyClient client.Client + EventLogger TCPEventLogger +} + +type TCPEventLogger interface { + Connect(addr, reqAddr net.Addr) + Error(addr, reqAddr net.Addr, err error) +} + +func (r *TCPRedirect) ListenAndServe(laddr *net.TCPAddr) error { + listener, err := net.ListenTCP("tcp", laddr) + if err != nil { + return err + } + defer listener.Close() + for { + c, err := listener.AcceptTCP() + if err != nil { + return err + } + go r.handle(c) + } +} + +func (r *TCPRedirect) handle(conn *net.TCPConn) { + defer conn.Close() + dstAddr, err := getDstAddr(conn) + if err != nil { + // Fail silently if we can't get the original destination. + // Maybe we should print something to the log? + return + } + if r.EventLogger != nil { + r.EventLogger.Connect(conn.RemoteAddr(), dstAddr) + } + var closeErr error + defer func() { + if r.EventLogger != nil { + r.EventLogger.Error(conn.RemoteAddr(), dstAddr, closeErr) + } + }() + + rc, err := r.HyClient.TCP(dstAddr.String()) + if err != nil { + closeErr = err + return + } + defer rc.Close() + + // Start forwarding + copyErrChan := make(chan error, 2) + go func() { + _, copyErr := io.Copy(rc, conn) + copyErrChan <- copyErr + }() + go func() { + _, copyErr := io.Copy(conn, rc) + copyErrChan <- copyErr + }() + closeErr = <-copyErrChan +} + +type sockAddr struct { + family uint16 + port [2]byte // always big endian regardless of platform + data [24]byte // sockaddr_in or sockaddr_in6 +} + +func getOriginalDst(fd uintptr) (*sockAddr, error) { + var addr sockAddr + addrSize := uint32(unsafe.Sizeof(addr)) + // Try IPv6 first + err := getsockopt(fd, syscall.SOL_IPV6, soOriginalDstV6, unsafe.Pointer(&addr), &addrSize) + if err == nil { + return &addr, nil + } + // Then IPv4 + err = getsockopt(fd, syscall.SOL_IP, soOriginalDst, unsafe.Pointer(&addr), &addrSize) + return &addr, err +} + +// getDstAddr returns the original destination of a redirected TCP connection. +func getDstAddr(conn *net.TCPConn) (*net.TCPAddr, error) { + rc, err := conn.SyscallConn() + if err != nil { + return nil, err + } + var addr *sockAddr + var err2 error + err = rc.Control(func(fd uintptr) { + addr, err2 = getOriginalDst(fd) + }) + if err != nil { + return nil, err + } + if err2 != nil { + return nil, err2 + } + switch addr.family { + case syscall.AF_INET: + return &net.TCPAddr{IP: addr.data[:4], Port: int(binary.BigEndian.Uint16(addr.port[:]))}, nil + case syscall.AF_INET6: + return &net.TCPAddr{IP: addr.data[4:20], Port: int(binary.BigEndian.Uint16(addr.port[:]))}, nil + default: + return nil, errors.New("address family not IPv4 or IPv6") + } +} diff --git a/v2rayng/hysteria/app/internal/redirect/tcp_others.go b/v2rayng/hysteria/app/internal/redirect/tcp_others.go new file mode 100644 index 0000000000..f5cf6f9446 --- /dev/null +++ b/v2rayng/hysteria/app/internal/redirect/tcp_others.go @@ -0,0 +1,24 @@ +//go:build !linux + +package redirect + +import ( + "errors" + "net" + + "github.com/apernet/hysteria/core/v2/client" +) + +type TCPRedirect struct { + HyClient client.Client + EventLogger TCPEventLogger +} + +type TCPEventLogger interface { + Connect(addr, reqAddr net.Addr) + Error(addr, reqAddr net.Addr, err error) +} + +func (r *TCPRedirect) ListenAndServe(laddr *net.TCPAddr) error { + return errors.New("not supported on this platform") +} diff --git a/v2rayng/hysteria/app/internal/sockopts/fd_control_unix_socket_test.py b/v2rayng/hysteria/app/internal/sockopts/fd_control_unix_socket_test.py new file mode 100644 index 0000000000..e47a6f64a1 --- /dev/null +++ b/v2rayng/hysteria/app/internal/sockopts/fd_control_unix_socket_test.py @@ -0,0 +1,65 @@ +import socket +import array +import os +import struct +import sys + + +def serve(path): + try: + os.unlink(path) + except OSError: + if os.path.exists(path): + raise + + server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + server.bind(path) + server.listen() + print(f"Listening on {path}") + + try: + while True: + connection, client_address = server.accept() + print(f"Client connected") + + try: + # Receiving fd from client + fds = array.array("i") + msg, ancdata, flags, addr = connection.recvmsg(1, socket.CMSG_LEN(struct.calcsize('i'))) + for cmsg_level, cmsg_type, cmsg_data in ancdata: + if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS: + fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)]) + + fd = fds[0] + + # We make a call to setsockopt(2) here, so client can verify we have received the fd + # In the real scenario, the server would set things like SO_MARK, + # we use SO_RCVBUF as it doesn't require any special capabilities. + nbytes = struct.pack("i", 2500) + fdsocket = fd_to_socket(fd) + fdsocket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, nbytes) + fdsocket.close() + + # The only protocol-like thing specified in the client implementation. + connection.send(b'\x01') + finally: + connection.close() + print("Connection closed") + + except KeyboardInterrupt: + print("Exit") + + finally: + server.close() + os.unlink(path) + + +def fd_to_socket(fd): + return socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM) + + +if __name__ == "__main__": + if len(sys.argv) < 2: + raise ValueError("unix socket path is required") + + serve(sys.argv[1]) diff --git a/v2rayng/hysteria/app/internal/sockopts/sockopts.go b/v2rayng/hysteria/app/internal/sockopts/sockopts.go new file mode 100644 index 0000000000..14ee0c019f --- /dev/null +++ b/v2rayng/hysteria/app/internal/sockopts/sockopts.go @@ -0,0 +1,76 @@ +package sockopts + +import ( + "fmt" + "net" +) + +type SocketOptions struct { + BindInterface *string + FirewallMark *uint32 + FdControlUnixSocket *string +} + +// implemented in platform-specific files +var ( + bindInterfaceFunc func(c *net.UDPConn, device string) error + firewallMarkFunc func(c *net.UDPConn, fwmark uint32) error + fdControlUnixSocketFunc func(c *net.UDPConn, path string) error +) + +func (o *SocketOptions) CheckSupported() (err error) { + if o.BindInterface != nil && bindInterfaceFunc == nil { + return &UnsupportedError{"bindInterface"} + } + if o.FirewallMark != nil && firewallMarkFunc == nil { + return &UnsupportedError{"fwmark"} + } + if o.FdControlUnixSocket != nil && fdControlUnixSocketFunc == nil { + return &UnsupportedError{"fdControlUnixSocket"} + } + return nil +} + +type UnsupportedError struct { + Field string +} + +func (e *UnsupportedError) Error() string { + return fmt.Sprintf("%s is not supported on this platform", e.Field) +} + +func (o *SocketOptions) ListenUDP() (uconn net.PacketConn, err error) { + uconn, err = net.ListenUDP("udp", nil) + if err != nil { + return + } + err = o.applyToUDPConn(uconn.(*net.UDPConn)) + if err != nil { + uconn.Close() + uconn = nil + return + } + return +} + +func (o *SocketOptions) applyToUDPConn(c *net.UDPConn) error { + if o.BindInterface != nil && bindInterfaceFunc != nil { + err := bindInterfaceFunc(c, *o.BindInterface) + if err != nil { + return fmt.Errorf("failed to bind to interface: %w", err) + } + } + if o.FirewallMark != nil && firewallMarkFunc != nil { + err := firewallMarkFunc(c, *o.FirewallMark) + if err != nil { + return fmt.Errorf("failed to set fwmark: %w", err) + } + } + if o.FdControlUnixSocket != nil && fdControlUnixSocketFunc != nil { + err := fdControlUnixSocketFunc(c, *o.FdControlUnixSocket) + if err != nil { + return fmt.Errorf("failed to send fd to control unix socket: %w", err) + } + } + return nil +} diff --git a/v2rayng/hysteria/app/internal/sockopts/sockopts_linux.go b/v2rayng/hysteria/app/internal/sockopts/sockopts_linux.go new file mode 100644 index 0000000000..d1e5d23c88 --- /dev/null +++ b/v2rayng/hysteria/app/internal/sockopts/sockopts_linux.go @@ -0,0 +1,96 @@ +//go:build linux + +package sockopts + +import ( + "fmt" + "net" + "time" + + "golang.org/x/exp/constraints" + "golang.org/x/sys/unix" +) + +const ( + fdControlUnixTimeout = 3 * time.Second +) + +func init() { + bindInterfaceFunc = bindInterfaceImpl + firewallMarkFunc = firewallMarkImpl + fdControlUnixSocketFunc = fdControlUnixSocketImpl +} + +func controlUDPConn(c *net.UDPConn, cb func(fd int) error) (err error) { + rconn, err := c.SyscallConn() + if err != nil { + return + } + cerr := rconn.Control(func(fd uintptr) { + err = cb(int(fd)) + }) + if err != nil { + return + } + if cerr != nil { + err = fmt.Errorf("failed to control fd: %w", cerr) + return + } + return +} + +func bindInterfaceImpl(c *net.UDPConn, device string) error { + return controlUDPConn(c, func(fd int) error { + return unix.BindToDevice(fd, device) + }) +} + +func firewallMarkImpl(c *net.UDPConn, fwmark uint32) error { + return controlUDPConn(c, func(fd int) error { + return unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_MARK, int(fwmark)) + }) +} + +func fdControlUnixSocketImpl(c *net.UDPConn, path string) error { + return controlUDPConn(c, func(fd int) error { + socketFd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM, 0) + if err != nil { + return fmt.Errorf("failed to create unix socket: %w", err) + } + defer unix.Close(socketFd) + + var timeout unix.Timeval + timeUsec := fdControlUnixTimeout.Microseconds() + castAssignInteger(timeUsec/1e6, &timeout.Sec) + // Specifying the type explicitly is not necessary here, but it makes GoLand happy. + castAssignInteger[int64](timeUsec%1e6, &timeout.Usec) + + _ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &timeout) + _ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_SNDTIMEO, &timeout) + + err = unix.Connect(socketFd, &unix.SockaddrUnix{Name: path}) + if err != nil { + return fmt.Errorf("failed to connect: %w", err) + } + + err = unix.Sendmsg(socketFd, nil, unix.UnixRights(fd), nil, 0) + if err != nil { + return fmt.Errorf("failed to send: %w", err) + } + + dummy := []byte{1} + n, err := unix.Read(socketFd, dummy) + if err != nil { + return fmt.Errorf("failed to receive: %w", err) + } + if n != 1 { + return fmt.Errorf("socket closed unexpectedly") + } + + return nil + }) +} + +func castAssignInteger[F, T constraints.Integer](from F, to *T) { + *to = T(from) +} diff --git a/v2rayng/hysteria/app/internal/sockopts/sockopts_linux_test.go b/v2rayng/hysteria/app/internal/sockopts/sockopts_linux_test.go new file mode 100644 index 0000000000..66614a4c7a --- /dev/null +++ b/v2rayng/hysteria/app/internal/sockopts/sockopts_linux_test.go @@ -0,0 +1,53 @@ +//go:build linux + +package sockopts + +import ( + "net" + "os" + "os/exec" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "golang.org/x/sys/unix" +) + +func Test_fdControlUnixSocketImpl(t *testing.T) { + sockPath := "./fd_control_unix_socket_test.sock" + defer os.Remove(sockPath) + + // Run test server + cmd := exec.Command("python", "fd_control_unix_socket_test.py", sockPath) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Start() + if !assert.NoError(t, err) { + return + } + defer cmd.Process.Kill() + + // Wait for the server to start + time.Sleep(1 * time.Second) + + so := SocketOptions{ + FdControlUnixSocket: &sockPath, + } + conn, err := so.ListenUDP() + if !assert.NoError(t, err) { + return + } + defer conn.Close() + + err = controlUDPConn(conn.(*net.UDPConn), func(fd int) (err error) { + rcvbuf, err := unix.GetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_RCVBUF) + if err != nil { + return + } + // The test server called setsockopt(fd, SOL_SOCKET, SO_RCVBUF, 2500), + // and kernel will double this value for getsockopt(). + assert.Equal(t, 5000, rcvbuf) + return + }) + assert.NoError(t, err) +} diff --git a/v2rayng/hysteria/app/internal/socks5/server.go b/v2rayng/hysteria/app/internal/socks5/server.go new file mode 100644 index 0000000000..0fc7de61ac --- /dev/null +++ b/v2rayng/hysteria/app/internal/socks5/server.go @@ -0,0 +1,300 @@ +package socks5 + +import ( + "encoding/binary" + "io" + "net" + + "github.com/txthinking/socks5" + + "github.com/apernet/hysteria/core/v2/client" +) + +const udpBufferSize = 4096 + +// Server is a SOCKS5 server using a Hysteria client as outbound. +type Server struct { + HyClient client.Client + AuthFunc func(username, password string) bool // nil = no authentication + DisableUDP bool + EventLogger EventLogger +} + +type EventLogger interface { + TCPRequest(addr net.Addr, reqAddr string) + TCPError(addr net.Addr, reqAddr string, err error) + UDPRequest(addr net.Addr) + UDPError(addr net.Addr, err error) +} + +func (s *Server) Serve(listener net.Listener) error { + for { + conn, err := listener.Accept() + if err != nil { + return err + } + go s.dispatch(conn) + } +} + +func (s *Server) dispatch(conn net.Conn) { + ok, _ := s.negotiate(conn) + if !ok { + _ = conn.Close() + return + } + // Negotiation ok, get and handle the request + req, err := socks5.NewRequestFrom(conn) + if err != nil { + _ = conn.Close() + return + } + switch req.Cmd { + case socks5.CmdConnect: // TCP + s.handleTCP(conn, req) + case socks5.CmdUDP: // UDP + if s.DisableUDP { + _ = sendSimpleReply(conn, socks5.RepCommandNotSupported) + _ = conn.Close() + return + } + s.handleUDP(conn, req) + default: + _ = sendSimpleReply(conn, socks5.RepCommandNotSupported) + _ = conn.Close() + } +} + +func (s *Server) negotiate(conn net.Conn) (bool, error) { + req, err := socks5.NewNegotiationRequestFrom(conn) + if err != nil { + return false, err + } + var serverMethod byte + if s.AuthFunc != nil { + serverMethod = socks5.MethodUsernamePassword + } else { + serverMethod = socks5.MethodNone + } + // Look for the supported method in the client request + supported := false + for _, m := range req.Methods { + if m == serverMethod { + supported = true + break + } + } + if !supported { + // No supported method found, reject the client + rep := socks5.NewNegotiationReply(socks5.MethodUnsupportAll) + _, err := rep.WriteTo(conn) + return false, err + } + // OK, send the method we chose + rep := socks5.NewNegotiationReply(serverMethod) + _, err = rep.WriteTo(conn) + if err != nil { + return false, err + } + // If we chose the username/password method, authenticate the client + if serverMethod == socks5.MethodUsernamePassword { + req, err := socks5.NewUserPassNegotiationRequestFrom(conn) + if err != nil { + return false, err + } + ok := s.AuthFunc(string(req.Uname), string(req.Passwd)) + if ok { + rep := socks5.NewUserPassNegotiationReply(socks5.UserPassStatusSuccess) + _, err := rep.WriteTo(conn) + if err != nil { + return false, err + } + } else { + rep := socks5.NewUserPassNegotiationReply(socks5.UserPassStatusFailure) + _, err := rep.WriteTo(conn) + return false, err + } + } + return true, nil +} + +func (s *Server) handleTCP(conn net.Conn, req *socks5.Request) { + defer conn.Close() + + addr := req.Address() + + // TCP request & error log + if s.EventLogger != nil { + s.EventLogger.TCPRequest(conn.RemoteAddr(), addr) + } + var closeErr error + defer func() { + if s.EventLogger != nil { + s.EventLogger.TCPError(conn.RemoteAddr(), addr, closeErr) + } + }() + + // Dial + rConn, err := s.HyClient.TCP(addr) + if err != nil { + _ = sendSimpleReply(conn, socks5.RepHostUnreachable) + closeErr = err + return + } + defer rConn.Close() + + // Send reply and start relaying + _ = sendSimpleReply(conn, socks5.RepSuccess) + copyErrChan := make(chan error, 2) + go func() { + _, err := io.Copy(rConn, conn) + copyErrChan <- err + }() + go func() { + _, err := io.Copy(conn, rConn) + copyErrChan <- err + }() + closeErr = <-copyErrChan +} + +func (s *Server) handleUDP(conn net.Conn, req *socks5.Request) { + defer conn.Close() + + // UDP request & error log + if s.EventLogger != nil { + s.EventLogger.UDPRequest(conn.RemoteAddr()) + } + var closeErr error + defer func() { + if s.EventLogger != nil { + s.EventLogger.UDPError(conn.RemoteAddr(), closeErr) + } + }() + + // Start UDP relay server + // SOCKS5 UDP requires the server to return the UDP bind address and port in the reply. + // We bind to the same address that our TCP server listens on (but a different port). + host, _, err := net.SplitHostPort(conn.LocalAddr().String()) + if err != nil { + // Is this even possible? + _ = sendSimpleReply(conn, socks5.RepServerFailure) + closeErr = err + return + } + udpAddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(host, "0")) + if err != nil { + _ = sendSimpleReply(conn, socks5.RepServerFailure) + closeErr = err + return + } + udpConn, err := net.ListenUDP("udp", udpAddr) + if err != nil { + _ = sendSimpleReply(conn, socks5.RepServerFailure) + closeErr = err + return + } + defer udpConn.Close() + + // HyClient UDP session + hyUDP, err := s.HyClient.UDP() + if err != nil { + _ = sendSimpleReply(conn, socks5.RepServerFailure) + closeErr = err + return + } + defer hyUDP.Close() + + // Send reply + _ = sendUDPReply(conn, udpConn.LocalAddr().(*net.UDPAddr)) + + // UDP relay & SOCKS5 connection holder + errChan := make(chan error, 2) + go func() { + err := s.udpServer(udpConn, hyUDP) + errChan <- err + }() + go func() { + _, err := io.Copy(io.Discard, conn) + errChan <- err + }() + closeErr = <-errChan +} + +func (s *Server) udpServer(udpConn *net.UDPConn, hyUDP client.HyUDPConn) error { + var clientAddr *net.UDPAddr + buf := make([]byte, udpBufferSize) + // local -> remote + for { + n, cAddr, err := udpConn.ReadFromUDP(buf) + if err != nil { + return err + } + d, err := socks5.NewDatagramFromBytes(buf[:n]) + if err != nil || d.Frag != 0 { + // Ignore bad packets + // Also we don't support SOCKS5 UDP fragmentation for now + continue + } + if clientAddr == nil { + // Before the first packet, we don't know what IP the client will use to send us packets, + // so we don't know what IP to return packets to. + // We treat whoever sends us the first packet as our client. + clientAddr = cAddr + // Now that we know the client's address, we can start the + // remote -> local direction. + go func() { + for { + bs, from, err := hyUDP.Receive() + if err != nil { + // Close the UDP conn so that the local -> remote direction will exit + _ = udpConn.Close() + return + } + atyp, addr, port, err := socks5.ParseAddress(from) + if err != nil { + continue + } + if atyp == socks5.ATYPDomain { + // socks5.ParseAddress adds a leading byte for domains, + // but socks5.NewDatagram will add it again as it expects a raw domain. + // So we must remove it here. + addr = addr[1:] + } + d := socks5.NewDatagram(atyp, addr, port, bs) + _, _ = udpConn.WriteToUDP(d.Bytes(), clientAddr) + } + }() + } else if !clientAddr.IP.Equal(cAddr.IP) || clientAddr.Port != cAddr.Port { + // Not our client, ignore + continue + } + // Send to remote + _ = hyUDP.Send(d.Data, d.Address()) + } +} + +// sendSimpleReply sends a SOCKS5 reply with the given reply code. +// It does not contain bind address or port, so it's not suitable for successful UDP requests. +func sendSimpleReply(conn net.Conn, rep byte) error { + p := socks5.NewReply(rep, socks5.ATYPIPv4, []byte{0x00, 0x00, 0x00, 0x00}, []byte{0x00, 0x00}) + _, err := p.WriteTo(conn) + return err +} + +// sendUDPReply sends a SOCKS5 reply with the given reply code and bind address/port. +func sendUDPReply(conn net.Conn, addr *net.UDPAddr) error { + var atyp byte + var bndAddr, bndPort []byte + if ip4 := addr.IP.To4(); ip4 != nil { + atyp = socks5.ATYPIPv4 + bndAddr = ip4 + } else { + atyp = socks5.ATYPIPv6 + bndAddr = addr.IP + } + bndPort = make([]byte, 2) + binary.BigEndian.PutUint16(bndPort, uint16(addr.Port)) + p := socks5.NewReply(socks5.RepSuccess, atyp, bndAddr, bndPort) + _, err := p.WriteTo(conn) + return err +} diff --git a/v2rayng/hysteria/app/internal/socks5/server_test.go b/v2rayng/hysteria/app/internal/socks5/server_test.go new file mode 100644 index 0000000000..1290058947 --- /dev/null +++ b/v2rayng/hysteria/app/internal/socks5/server_test.go @@ -0,0 +1,29 @@ +package socks5 + +import ( + "net" + "os/exec" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/apernet/hysteria/app/v2/internal/utils_test" +) + +func TestServer(t *testing.T) { + // Start the server + l, err := net.Listen("tcp", "127.0.0.1:11080") + assert.NoError(t, err) + defer l.Close() + s := &Server{ + HyClient: &utils_test.MockEchoHyClient{}, + } + go s.Serve(l) + + // Run the Python test script + cmd := exec.Command("python", "server_test.py") + out, err := cmd.CombinedOutput() + assert.NoError(t, err) + assert.Equal(t, "OK", strings.TrimSpace(string(out))) +} diff --git a/v2rayng/hysteria/app/internal/socks5/server_test.py b/v2rayng/hysteria/app/internal/socks5/server_test.py new file mode 100644 index 0000000000..39f98bcbe5 --- /dev/null +++ b/v2rayng/hysteria/app/internal/socks5/server_test.py @@ -0,0 +1,57 @@ +import socket +import socks +import os + +ADDR = "127.0.0.1" +PORT = 11080 + + +def test_tcp(size, count, it, domain=False): + for i in range(it): + s = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM) + s.set_proxy(socks.SOCKS5, ADDR, PORT) + + if domain: + s.connect(("test.tcp.com", 12345)) + else: + s.connect(("1.2.3.4", 12345)) + + for j in range(count): + payload = os.urandom(size) + s.send(payload) + rsp = s.recv(size) + assert rsp == payload + + s.close() + + +def test_udp(size, count, it, domain=False): + for i in range(it): + s = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM) + s.set_proxy(socks.SOCKS5, ADDR, PORT) + + for j in range(count): + payload = os.urandom(size) + + if domain: + s.sendto(payload, ("test.udp.com", 12345)) + else: + s.sendto(payload, ("1.2.3.4", 12345)) + + rsp, addr = s.recvfrom(size) + assert rsp == payload + + if domain: + assert addr == (b"test.udp.com", 12345) + else: + assert addr == ("1.2.3.4", 12345) + + s.close() + + +if __name__ == "__main__": + test_tcp(1024, 1024, 10, domain=False) + test_tcp(1024, 1024, 10, domain=True) + test_udp(1024, 1024, 10, domain=False) + test_udp(1024, 1024, 10, domain=True) + print("OK") diff --git a/v2rayng/hysteria/app/internal/tproxy/tcp_linux.go b/v2rayng/hysteria/app/internal/tproxy/tcp_linux.go new file mode 100644 index 0000000000..4d8c1e9df4 --- /dev/null +++ b/v2rayng/hysteria/app/internal/tproxy/tcp_linux.go @@ -0,0 +1,69 @@ +package tproxy + +import ( + "io" + "net" + + "github.com/apernet/go-tproxy" + "github.com/apernet/hysteria/core/v2/client" +) + +type TCPTProxy struct { + HyClient client.Client + EventLogger TCPEventLogger +} + +type TCPEventLogger interface { + Connect(addr, reqAddr net.Addr) + Error(addr, reqAddr net.Addr, err error) +} + +func (r *TCPTProxy) ListenAndServe(laddr *net.TCPAddr) error { + listener, err := tproxy.ListenTCP("tcp", laddr) + if err != nil { + return err + } + defer listener.Close() + for { + c, err := listener.Accept() + if err != nil { + return err + } + go r.handle(c) + } +} + +func (r *TCPTProxy) handle(conn net.Conn) { + defer conn.Close() + // In TProxy mode, we are masquerading as the remote server. + // So LocalAddr is actually the target the user is trying to connect to, + // and RemoteAddr is the local address. + if r.EventLogger != nil { + r.EventLogger.Connect(conn.RemoteAddr(), conn.LocalAddr()) + } + var closeErr error + defer func() { + if r.EventLogger != nil { + r.EventLogger.Error(conn.RemoteAddr(), conn.LocalAddr(), closeErr) + } + }() + + rc, err := r.HyClient.TCP(conn.LocalAddr().String()) + if err != nil { + closeErr = err + return + } + defer rc.Close() + + // Start forwarding + copyErrChan := make(chan error, 2) + go func() { + _, copyErr := io.Copy(rc, conn) + copyErrChan <- copyErr + }() + go func() { + _, copyErr := io.Copy(conn, rc) + copyErrChan <- copyErr + }() + closeErr = <-copyErrChan +} diff --git a/v2rayng/hysteria/app/internal/tproxy/tcp_others.go b/v2rayng/hysteria/app/internal/tproxy/tcp_others.go new file mode 100644 index 0000000000..f33de6a9f3 --- /dev/null +++ b/v2rayng/hysteria/app/internal/tproxy/tcp_others.go @@ -0,0 +1,24 @@ +//go:build !linux + +package tproxy + +import ( + "errors" + "net" + + "github.com/apernet/hysteria/core/v2/client" +) + +type TCPTProxy struct { + HyClient client.Client + EventLogger TCPEventLogger +} + +type TCPEventLogger interface { + Connect(addr, reqAddr net.Addr) + Error(addr, reqAddr net.Addr, err error) +} + +func (r *TCPTProxy) ListenAndServe(laddr *net.TCPAddr) error { + return errors.New("not supported on this platform") +} diff --git a/v2rayng/hysteria/app/internal/tproxy/udp_linux.go b/v2rayng/hysteria/app/internal/tproxy/udp_linux.go new file mode 100644 index 0000000000..827bea24d7 --- /dev/null +++ b/v2rayng/hysteria/app/internal/tproxy/udp_linux.go @@ -0,0 +1,140 @@ +package tproxy + +import ( + "errors" + "net" + "time" + + "github.com/apernet/go-tproxy" + "github.com/apernet/hysteria/core/v2/client" +) + +const ( + udpBufferSize = 4096 + defaultTimeout = 60 * time.Second +) + +type UDPTProxy struct { + HyClient client.Client + Timeout time.Duration + EventLogger UDPEventLogger +} + +type UDPEventLogger interface { + Connect(addr, reqAddr net.Addr) + Error(addr, reqAddr net.Addr, err error) +} + +func (r *UDPTProxy) ListenAndServe(laddr *net.UDPAddr) error { + conn, err := tproxy.ListenUDP("udp", laddr) + if err != nil { + return err + } + defer conn.Close() + buf := make([]byte, udpBufferSize) + for { + // We will only get the first packet of each src/dst pair here, + // because newPair will create a TProxy connection and take over + // the src/dst pair. Later packets will be sent there instead of here. + n, srcAddr, dstAddr, err := tproxy.ReadFromUDP(conn, buf) + if err != nil { + return err + } + r.newPair(srcAddr, dstAddr, buf[:n]) + } +} + +func (r *UDPTProxy) newPair(srcAddr, dstAddr *net.UDPAddr, initPkt []byte) { + if r.EventLogger != nil { + r.EventLogger.Connect(srcAddr, dstAddr) + } + var closeErr error + defer func() { + // If closeErr is nil, it means we at least successfully sent the first packet + // and started forwarding, in which case we don't call the error logger. + if r.EventLogger != nil && closeErr != nil { + r.EventLogger.Error(srcAddr, dstAddr, closeErr) + } + }() + conn, err := tproxy.DialUDP("udp", dstAddr, srcAddr) + if err != nil { + closeErr = err + return + } + hyConn, err := r.HyClient.UDP() + if err != nil { + _ = conn.Close() + closeErr = err + return + } + // Send the first packet + err = hyConn.Send(initPkt, dstAddr.String()) + if err != nil { + _ = conn.Close() + _ = hyConn.Close() + closeErr = err + return + } + // Start forwarding + go func() { + err := r.forwarding(conn, hyConn, dstAddr.String()) + _ = conn.Close() + _ = hyConn.Close() + if r.EventLogger != nil { + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + // We don't consider deadline exceeded (timeout) an error + err = nil + } + r.EventLogger.Error(srcAddr, dstAddr, err) + } + }() +} + +func (r *UDPTProxy) forwarding(conn *net.UDPConn, hyConn client.HyUDPConn, dst string) error { + errChan := make(chan error, 2) + // Local <- Remote + go func() { + for { + bs, _, err := hyConn.Receive() + if err != nil { + errChan <- err + return + } + _, err = conn.Write(bs) + if err != nil { + errChan <- err + return + } + _ = r.updateConnDeadline(conn) + } + }() + // Local -> Remote + go func() { + buf := make([]byte, udpBufferSize) + for { + _ = r.updateConnDeadline(conn) + n, err := conn.Read(buf) + if n > 0 { + err := hyConn.Send(buf[:n], dst) + if err != nil { + errChan <- err + return + } + } + if err != nil { + errChan <- err + return + } + } + }() + return <-errChan +} + +func (r *UDPTProxy) updateConnDeadline(conn *net.UDPConn) error { + if r.Timeout == 0 { + return conn.SetReadDeadline(time.Now().Add(defaultTimeout)) + } else { + return conn.SetReadDeadline(time.Now().Add(r.Timeout)) + } +} diff --git a/v2rayng/hysteria/app/internal/tproxy/udp_others.go b/v2rayng/hysteria/app/internal/tproxy/udp_others.go new file mode 100644 index 0000000000..3d267e63f4 --- /dev/null +++ b/v2rayng/hysteria/app/internal/tproxy/udp_others.go @@ -0,0 +1,26 @@ +//go:build !linux + +package tproxy + +import ( + "errors" + "net" + "time" + + "github.com/apernet/hysteria/core/v2/client" +) + +type UDPTProxy struct { + HyClient client.Client + Timeout time.Duration + EventLogger UDPEventLogger +} + +type UDPEventLogger interface { + Connect(addr, reqAddr net.Addr) + Error(addr, reqAddr net.Addr, err error) +} + +func (r *UDPTProxy) ListenAndServe(laddr *net.UDPAddr) error { + return errors.New("not supported on this platform") +} diff --git a/v2rayng/hysteria/app/internal/tun/log.go b/v2rayng/hysteria/app/internal/tun/log.go new file mode 100644 index 0000000000..b30309d28d --- /dev/null +++ b/v2rayng/hysteria/app/internal/tun/log.go @@ -0,0 +1,77 @@ +package tun + +import ( + "github.com/sagernet/sing/common/logger" + "go.uber.org/zap" +) + +var _ logger.Logger = (*singLogger)(nil) + +type singLogger struct { + tag string + zapLogger *zap.Logger +} + +func extractSingExceptions(args []any) { + for i, arg := range args { + if err, ok := arg.(error); ok { + args[i] = err.Error() + } + } +} + +func (l *singLogger) Trace(args ...any) { + if l.zapLogger == nil { + return + } + extractSingExceptions(args) + l.zapLogger.Debug(l.tag, zap.Any("args", args)) +} + +func (l *singLogger) Debug(args ...any) { + if l.zapLogger == nil { + return + } + extractSingExceptions(args) + l.zapLogger.Debug(l.tag, zap.Any("args", args)) +} + +func (l *singLogger) Info(args ...any) { + if l.zapLogger == nil { + return + } + extractSingExceptions(args) + l.zapLogger.Info(l.tag, zap.Any("args", args)) +} + +func (l *singLogger) Warn(args ...any) { + if l.zapLogger == nil { + return + } + extractSingExceptions(args) + l.zapLogger.Warn(l.tag, zap.Any("args", args)) +} + +func (l *singLogger) Error(args ...any) { + if l.zapLogger == nil { + return + } + extractSingExceptions(args) + l.zapLogger.Error(l.tag, zap.Any("args", args)) +} + +func (l *singLogger) Fatal(args ...any) { + if l.zapLogger == nil { + return + } + extractSingExceptions(args) + l.zapLogger.Fatal(l.tag, zap.Any("args", args)) +} + +func (l *singLogger) Panic(args ...any) { + if l.zapLogger == nil { + return + } + extractSingExceptions(args) + l.zapLogger.Panic(l.tag, zap.Any("args", args)) +} diff --git a/v2rayng/hysteria/app/internal/tun/server.go b/v2rayng/hysteria/app/internal/tun/server.go new file mode 100644 index 0000000000..303d4ec567 --- /dev/null +++ b/v2rayng/hysteria/app/internal/tun/server.go @@ -0,0 +1,230 @@ +package tun + +import ( + "context" + "fmt" + "io" + "net" + "net/netip" + + tun "github.com/apernet/sing-tun" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/control" + "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/network" + "go.uber.org/zap" + + "github.com/apernet/hysteria/core/v2/client" +) + +type Server struct { + HyClient client.Client + EventLogger EventLogger + + // for debugging + Logger *zap.Logger + + IfName string + MTU uint32 + Timeout int64 // in seconds, also applied to TCP in system stack + + // required by system stack + Inet4Address []netip.Prefix + Inet6Address []netip.Prefix + + // auto route + AutoRoute bool + StructRoute bool + Inet4RouteAddress []netip.Prefix + Inet6RouteAddress []netip.Prefix + Inet4RouteExcludeAddress []netip.Prefix + Inet6RouteExcludeAddress []netip.Prefix +} + +type EventLogger interface { + TCPRequest(addr, reqAddr string) + TCPError(addr, reqAddr string, err error) + UDPRequest(addr string) + UDPError(addr string, err error) +} + +func (s *Server) Serve() error { + tunOpts := tun.Options{ + Name: s.IfName, + Inet4Address: s.Inet4Address, + Inet6Address: s.Inet6Address, + MTU: s.MTU, + GSO: true, + AutoRoute: s.AutoRoute, + StrictRoute: s.StructRoute, + Inet4RouteAddress: s.Inet4RouteAddress, + Inet6RouteAddress: s.Inet6RouteAddress, + Inet4RouteExcludeAddress: s.Inet4RouteExcludeAddress, + Inet6RouteExcludeAddress: s.Inet6RouteExcludeAddress, + Logger: &singLogger{ + tag: "tun", + zapLogger: s.Logger, + }, + } + tunIf, err := tun.New(tunOpts) + if err != nil { + return fmt.Errorf("failed to create tun interface: %w", err) + } + defer tunIf.Close() + + tunStack, err := tun.NewSystem(tun.StackOptions{ + Context: context.Background(), + Tun: tunIf, + TunOptions: tunOpts, + UDPTimeout: s.Timeout, + Handler: &tunHandler{s}, + Logger: &singLogger{ + tag: "tun-stack", + zapLogger: s.Logger, + }, + ForwarderBindInterface: true, + InterfaceFinder: &interfaceFinder{}, + }) + if err != nil { + return fmt.Errorf("failed to create tun stack: %w", err) + } + defer tunStack.Close() + return tunStack.(tun.StackRunner).Run() +} + +type tunHandler struct { + *Server +} + +var _ tun.Handler = (*tunHandler)(nil) + +func (t *tunHandler) NewConnection(ctx context.Context, conn net.Conn, m metadata.Metadata) error { + addr := m.Source.String() + reqAddr := m.Destination.String() + if t.EventLogger != nil { + t.EventLogger.TCPRequest(addr, reqAddr) + } + var closeErr error + defer func() { + if t.EventLogger != nil { + t.EventLogger.TCPError(addr, reqAddr, closeErr) + } + }() + rc, err := t.HyClient.TCP(reqAddr) + if err != nil { + closeErr = err + // the returned err is ignored by caller + return nil + } + defer rc.Close() + + // start forwarding + copyErrChan := make(chan error, 3) + go func() { + <-ctx.Done() + copyErrChan <- ctx.Err() + }() + go func() { + _, copyErr := io.Copy(rc, conn) + copyErrChan <- copyErr + }() + go func() { + _, copyErr := io.Copy(conn, rc) + copyErrChan <- copyErr + }() + closeErr = <-copyErrChan + return nil +} + +func (t *tunHandler) NewPacketConnection(ctx context.Context, conn network.PacketConn, m metadata.Metadata) error { + addr := m.Source.String() + if t.EventLogger != nil { + t.EventLogger.UDPRequest(addr) + } + var closeErr error + defer func() { + if t.EventLogger != nil { + t.EventLogger.UDPError(addr, closeErr) + } + }() + rc, err := t.HyClient.UDP() + if err != nil { + closeErr = err + // the returned err is simply called into NewError again + return nil + } + defer rc.Close() + + // start forwarding + copyErrChan := make(chan error, 3) + go func() { + <-ctx.Done() + copyErrChan <- ctx.Err() + }() + // local <- remote + go func() { + for { + bs, from, err := rc.Receive() + if err != nil { + copyErrChan <- err + return + } + var fromAddr metadata.Socksaddr + if ap, perr := netip.ParseAddrPort(from); perr == nil { + fromAddr = metadata.SocksaddrFromNetIP(ap) + } else { + fromAddr.Fqdn = from + } + err = conn.WritePacket(buf.As(bs), fromAddr) + if err != nil { + copyErrChan <- err + return + } + } + }() + // local -> remote + go func() { + buffer := buf.NewPacket() + defer buffer.Release() + + for { + buffer.Reset() + addr, err := conn.ReadPacket(buffer) + if err != nil { + copyErrChan <- err + return + } + err = rc.Send(buffer.Bytes(), addr.String()) + if err != nil { + copyErrChan <- err + return + } + } + }() + closeErr = <-copyErrChan + return nil +} + +func (t *tunHandler) NewError(ctx context.Context, err error) { + // unused +} + +type interfaceFinder struct{} + +var _ control.InterfaceFinder = (*interfaceFinder)(nil) + +func (f *interfaceFinder) InterfaceIndexByName(name string) (int, error) { + ifce, err := net.InterfaceByName(name) + if err != nil { + return -1, err + } + return ifce.Index, nil +} + +func (f *interfaceFinder) InterfaceNameByIndex(index int) (string, error) { + ifce, err := net.InterfaceByIndex(index) + if err != nil { + return "", err + } + return ifce.Name, nil +} diff --git a/v2rayng/hysteria/app/internal/url/url.go b/v2rayng/hysteria/app/internal/url/url.go new file mode 100644 index 0000000000..c9e1bd0313 --- /dev/null +++ b/v2rayng/hysteria/app/internal/url/url.go @@ -0,0 +1,1270 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package url parses URLs and implements query escaping. +package url + +// See RFC 3986. This package generally follows RFC 3986, except where +// it deviates for compatibility reasons. When sending changes, first +// search old issues for history on decisions. Unit tests should also +// contain references to issue numbers with details. + +// Hysteria fork note: This file is grabbed from the standard Go library, +// but with a few modifications to make it support our special port format +// when using port hopping. + +import ( + "errors" + "fmt" + "path" + "sort" + "strconv" + "strings" +) + +// Error reports an error and the operation and URL that caused it. +type Error struct { + Op string + URL string + Err error +} + +func (e *Error) Unwrap() error { return e.Err } +func (e *Error) Error() string { return fmt.Sprintf("%s %q: %s", e.Op, e.URL, e.Err) } + +func (e *Error) Timeout() bool { + t, ok := e.Err.(interface { + Timeout() bool + }) + return ok && t.Timeout() +} + +func (e *Error) Temporary() bool { + t, ok := e.Err.(interface { + Temporary() bool + }) + return ok && t.Temporary() +} + +const upperhex = "0123456789ABCDEF" + +func ishex(c byte) bool { + switch { + case '0' <= c && c <= '9': + return true + case 'a' <= c && c <= 'f': + return true + case 'A' <= c && c <= 'F': + return true + } + return false +} + +func unhex(c byte) byte { + switch { + case '0' <= c && c <= '9': + return c - '0' + case 'a' <= c && c <= 'f': + return c - 'a' + 10 + case 'A' <= c && c <= 'F': + return c - 'A' + 10 + } + return 0 +} + +type encoding int + +const ( + encodePath encoding = 1 + iota + encodePathSegment + encodeHost + encodeZone + encodeUserPassword + encodeQueryComponent + encodeFragment +) + +type EscapeError string + +func (e EscapeError) Error() string { + return "invalid URL escape " + strconv.Quote(string(e)) +} + +type InvalidHostError string + +func (e InvalidHostError) Error() string { + return "invalid character " + strconv.Quote(string(e)) + " in host name" +} + +// Return true if the specified character should be escaped when +// appearing in a URL string, according to RFC 3986. +// +// Please be informed that for now shouldEscape does not check all +// reserved characters correctly. See golang.org/issue/5684. +func shouldEscape(c byte, mode encoding) bool { + // §2.3 Unreserved characters (alphanum) + if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' { + return false + } + + if mode == encodeHost || mode == encodeZone { + // §3.2.2 Host allows + // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" + // as part of reg-name. + // We add : because we include :port as part of host. + // We add [ ] because we include [ipv6]:port as part of host. + // We add < > because they're the only characters left that + // we could possibly allow, and Parse will reject them if we + // escape them (because hosts can't use %-encoding for + // ASCII bytes). + switch c { + case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '[', ']', '<', '>', '"': + return false + } + } + + switch c { + case '-', '_', '.', '~': // §2.3 Unreserved characters (mark) + return false + + case '$', '&', '+', ',', '/', ':', ';', '=', '?', '@': // §2.2 Reserved characters (reserved) + // Different sections of the URL allow a few of + // the reserved characters to appear unescaped. + switch mode { + case encodePath: // §3.3 + // The RFC allows : @ & = + $ but saves / ; , for assigning + // meaning to individual path segments. This package + // only manipulates the path as a whole, so we allow those + // last three as well. That leaves only ? to escape. + return c == '?' + + case encodePathSegment: // §3.3 + // The RFC allows : @ & = + $ but saves / ; , for assigning + // meaning to individual path segments. + return c == '/' || c == ';' || c == ',' || c == '?' + + case encodeUserPassword: // §3.2.1 + // The RFC allows ';', ':', '&', '=', '+', '$', and ',' in + // userinfo, so we must escape only '@', '/', and '?'. + // The parsing of userinfo treats ':' as special so we must escape + // that too. + return c == '@' || c == '/' || c == '?' || c == ':' + + case encodeQueryComponent: // §3.4 + // The RFC reserves (so we must escape) everything. + return true + + case encodeFragment: // §4.1 + // The RFC text is silent but the grammar allows + // everything, so escape nothing. + return false + } + } + + if mode == encodeFragment { + // RFC 3986 §2.2 allows not escaping sub-delims. A subset of sub-delims are + // included in reserved from RFC 2396 §2.2. The remaining sub-delims do not + // need to be escaped. To minimize potential breakage, we apply two restrictions: + // (1) we always escape sub-delims outside of the fragment, and (2) we always + // escape single quote to avoid breaking callers that had previously assumed that + // single quotes would be escaped. See issue #19917. + switch c { + case '!', '(', ')', '*': + return false + } + } + + // Everything else must be escaped. + return true +} + +// QueryUnescape does the inverse transformation of QueryEscape, +// converting each 3-byte encoded substring of the form "%AB" into the +// hex-decoded byte 0xAB. +// It returns an error if any % is not followed by two hexadecimal +// digits. +func QueryUnescape(s string) (string, error) { + return unescape(s, encodeQueryComponent) +} + +// PathUnescape does the inverse transformation of PathEscape, +// converting each 3-byte encoded substring of the form "%AB" into the +// hex-decoded byte 0xAB. It returns an error if any % is not followed +// by two hexadecimal digits. +// +// PathUnescape is identical to QueryUnescape except that it does not +// unescape '+' to ' ' (space). +func PathUnescape(s string) (string, error) { + return unescape(s, encodePathSegment) +} + +// unescape unescapes a string; the mode specifies +// which section of the URL string is being unescaped. +func unescape(s string, mode encoding) (string, error) { + // Count %, check that they're well-formed. + n := 0 + hasPlus := false + for i := 0; i < len(s); { + switch s[i] { + case '%': + n++ + if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) { + s = s[i:] + if len(s) > 3 { + s = s[:3] + } + return "", EscapeError(s) + } + // Per https://tools.ietf.org/html/rfc3986#page-21 + // in the host component %-encoding can only be used + // for non-ASCII bytes. + // But https://tools.ietf.org/html/rfc6874#section-2 + // introduces %25 being allowed to escape a percent sign + // in IPv6 scoped-address literals. Yay. + if mode == encodeHost && unhex(s[i+1]) < 8 && s[i:i+3] != "%25" { + return "", EscapeError(s[i : i+3]) + } + if mode == encodeZone { + // RFC 6874 says basically "anything goes" for zone identifiers + // and that even non-ASCII can be redundantly escaped, + // but it seems prudent to restrict %-escaped bytes here to those + // that are valid host name bytes in their unescaped form. + // That is, you can use escaping in the zone identifier but not + // to introduce bytes you couldn't just write directly. + // But Windows puts spaces here! Yay. + v := unhex(s[i+1])<<4 | unhex(s[i+2]) + if s[i:i+3] != "%25" && v != ' ' && shouldEscape(v, encodeHost) { + return "", EscapeError(s[i : i+3]) + } + } + i += 3 + case '+': + hasPlus = mode == encodeQueryComponent + i++ + default: + if (mode == encodeHost || mode == encodeZone) && s[i] < 0x80 && shouldEscape(s[i], mode) { + return "", InvalidHostError(s[i : i+1]) + } + i++ + } + } + + if n == 0 && !hasPlus { + return s, nil + } + + var t strings.Builder + t.Grow(len(s) - 2*n) + for i := 0; i < len(s); i++ { + switch s[i] { + case '%': + t.WriteByte(unhex(s[i+1])<<4 | unhex(s[i+2])) + i += 2 + case '+': + if mode == encodeQueryComponent { + t.WriteByte(' ') + } else { + t.WriteByte('+') + } + default: + t.WriteByte(s[i]) + } + } + return t.String(), nil +} + +// QueryEscape escapes the string so it can be safely placed +// inside a URL query. +func QueryEscape(s string) string { + return escape(s, encodeQueryComponent) +} + +// PathEscape escapes the string so it can be safely placed inside a URL path segment, +// replacing special characters (including /) with %XX sequences as needed. +func PathEscape(s string) string { + return escape(s, encodePathSegment) +} + +func escape(s string, mode encoding) string { + spaceCount, hexCount := 0, 0 + for i := 0; i < len(s); i++ { + c := s[i] + if shouldEscape(c, mode) { + if c == ' ' && mode == encodeQueryComponent { + spaceCount++ + } else { + hexCount++ + } + } + } + + if spaceCount == 0 && hexCount == 0 { + return s + } + + var buf [64]byte + var t []byte + + required := len(s) + 2*hexCount + if required <= len(buf) { + t = buf[:required] + } else { + t = make([]byte, required) + } + + if hexCount == 0 { + copy(t, s) + for i := 0; i < len(s); i++ { + if s[i] == ' ' { + t[i] = '+' + } + } + return string(t) + } + + j := 0 + for i := 0; i < len(s); i++ { + switch c := s[i]; { + case c == ' ' && mode == encodeQueryComponent: + t[j] = '+' + j++ + case shouldEscape(c, mode): + t[j] = '%' + t[j+1] = upperhex[c>>4] + t[j+2] = upperhex[c&15] + j += 3 + default: + t[j] = s[i] + j++ + } + } + return string(t) +} + +// A URL represents a parsed URL (technically, a URI reference). +// +// The general form represented is: +// +// [scheme:][//[userinfo@]host][/]path[?query][#fragment] +// +// URLs that do not start with a slash after the scheme are interpreted as: +// +// scheme:opaque[?query][#fragment] +// +// Note that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/. +// A consequence is that it is impossible to tell which slashes in the Path were +// slashes in the raw URL and which were %2f. This distinction is rarely important, +// but when it is, the code should use the EscapedPath method, which preserves +// the original encoding of Path. +// +// The RawPath field is an optional field which is only set when the default +// encoding of Path is different from the escaped path. See the EscapedPath method +// for more details. +// +// URL's String method uses the EscapedPath method to obtain the path. +type URL struct { + Scheme string + Opaque string // encoded opaque data + User *Userinfo // username and password information + Host string // host or host:port + Path string // path (relative paths may omit leading slash) + RawPath string // encoded path hint (see EscapedPath method) + OmitHost bool // do not emit empty host (authority) + ForceQuery bool // append a query ('?') even if RawQuery is empty + RawQuery string // encoded query values, without '?' + Fragment string // fragment for references, without '#' + RawFragment string // encoded fragment hint (see EscapedFragment method) +} + +// User returns a Userinfo containing the provided username +// and no password set. +func User(username string) *Userinfo { + return &Userinfo{username, "", false} +} + +// UserPassword returns a Userinfo containing the provided username +// and password. +// +// This functionality should only be used with legacy web sites. +// RFC 2396 warns that interpreting Userinfo this way +// “is NOT RECOMMENDED, because the passing of authentication +// information in clear text (such as URI) has proven to be a +// security risk in almost every case where it has been used.” +func UserPassword(username, password string) *Userinfo { + return &Userinfo{username, password, true} +} + +// The Userinfo type is an immutable encapsulation of username and +// password details for a URL. An existing Userinfo value is guaranteed +// to have a username set (potentially empty, as allowed by RFC 2396), +// and optionally a password. +type Userinfo struct { + username string + password string + passwordSet bool +} + +// Username returns the username. +func (u *Userinfo) Username() string { + if u == nil { + return "" + } + return u.username +} + +// Password returns the password in case it is set, and whether it is set. +func (u *Userinfo) Password() (string, bool) { + if u == nil { + return "", false + } + return u.password, u.passwordSet +} + +// String returns the encoded userinfo information in the standard form +// of "username[:password]". +func (u *Userinfo) String() string { + if u == nil { + return "" + } + s := escape(u.username, encodeUserPassword) + if u.passwordSet { + s += ":" + escape(u.password, encodeUserPassword) + } + return s +} + +// Maybe rawURL is of the form scheme:path. +// (Scheme must be [a-zA-Z][a-zA-Z0-9+.-]*) +// If so, return scheme, path; else return "", rawURL. +func getScheme(rawURL string) (scheme, path string, err error) { + for i := 0; i < len(rawURL); i++ { + c := rawURL[i] + switch { + case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z': + // do nothing + case '0' <= c && c <= '9' || c == '+' || c == '-' || c == '.': + if i == 0 { + return "", rawURL, nil + } + case c == ':': + if i == 0 { + return "", "", errors.New("missing protocol scheme") + } + return rawURL[:i], rawURL[i+1:], nil + default: + // we have encountered an invalid character, + // so there is no valid scheme + return "", rawURL, nil + } + } + return "", rawURL, nil +} + +// Parse parses a raw url into a URL structure. +// +// The url may be relative (a path, without a host) or absolute +// (starting with a scheme). Trying to parse a hostname and path +// without a scheme is invalid but may not necessarily return an +// error, due to parsing ambiguities. +func Parse(rawURL string) (*URL, error) { + // Cut off #frag + u, frag, _ := strings.Cut(rawURL, "#") + url, err := parse(u, false) + if err != nil { + return nil, &Error{"parse", u, err} + } + if frag == "" { + return url, nil + } + if err = url.setFragment(frag); err != nil { + return nil, &Error{"parse", rawURL, err} + } + return url, nil +} + +// ParseRequestURI parses a raw url into a URL structure. It assumes that +// url was received in an HTTP request, so the url is interpreted +// only as an absolute URI or an absolute path. +// The string url is assumed not to have a #fragment suffix. +// (Web browsers strip #fragment before sending the URL to a web server.) +func ParseRequestURI(rawURL string) (*URL, error) { + url, err := parse(rawURL, true) + if err != nil { + return nil, &Error{"parse", rawURL, err} + } + return url, nil +} + +// parse parses a URL from a string in one of two contexts. If +// viaRequest is true, the URL is assumed to have arrived via an HTTP request, +// in which case only absolute URLs or path-absolute relative URLs are allowed. +// If viaRequest is false, all forms of relative URLs are allowed. +func parse(rawURL string, viaRequest bool) (*URL, error) { + var rest string + var err error + + if stringContainsCTLByte(rawURL) { + return nil, errors.New("net/url: invalid control character in URL") + } + + if rawURL == "" && viaRequest { + return nil, errors.New("empty url") + } + url := new(URL) + + if rawURL == "*" { + url.Path = "*" + return url, nil + } + + // Split off possible leading "http:", "mailto:", etc. + // Cannot contain escaped characters. + if url.Scheme, rest, err = getScheme(rawURL); err != nil { + return nil, err + } + url.Scheme = strings.ToLower(url.Scheme) + + if strings.HasSuffix(rest, "?") && strings.Count(rest, "?") == 1 { + url.ForceQuery = true + rest = rest[:len(rest)-1] + } else { + rest, url.RawQuery, _ = strings.Cut(rest, "?") + } + + if !strings.HasPrefix(rest, "/") { + if url.Scheme != "" { + // We consider rootless paths per RFC 3986 as opaque. + url.Opaque = rest + return url, nil + } + if viaRequest { + return nil, errors.New("invalid URI for request") + } + + // Avoid confusion with malformed schemes, like cache_object:foo/bar. + // See golang.org/issue/16822. + // + // RFC 3986, §3.3: + // In addition, a URI reference (Section 4.1) may be a relative-path reference, + // in which case the first path segment cannot contain a colon (":") character. + if segment, _, _ := strings.Cut(rest, "/"); strings.Contains(segment, ":") { + // First path segment has colon. Not allowed in relative URL. + return nil, errors.New("first path segment in URL cannot contain colon") + } + } + + if (url.Scheme != "" || !viaRequest && !strings.HasPrefix(rest, "///")) && strings.HasPrefix(rest, "//") { + var authority string + authority, rest = rest[2:], "" + if i := strings.Index(authority, "/"); i >= 0 { + authority, rest = authority[:i], authority[i:] + } + url.User, url.Host, err = parseAuthority(authority) + if err != nil { + return nil, err + } + } else if url.Scheme != "" && strings.HasPrefix(rest, "/") { + // OmitHost is set to true when rawURL has an empty host (authority). + // See golang.org/issue/46059. + url.OmitHost = true + } + + // Set Path and, optionally, RawPath. + // RawPath is a hint of the encoding of Path. We don't want to set it if + // the default escaping of Path is equivalent, to help make sure that people + // don't rely on it in general. + if err := url.setPath(rest); err != nil { + return nil, err + } + return url, nil +} + +func parseAuthority(authority string) (user *Userinfo, host string, err error) { + i := strings.LastIndex(authority, "@") + if i < 0 { + host, err = parseHost(authority) + } else { + host, err = parseHost(authority[i+1:]) + } + if err != nil { + return nil, "", err + } + if i < 0 { + return nil, host, nil + } + userinfo := authority[:i] + if !validUserinfo(userinfo) { + return nil, "", errors.New("net/url: invalid userinfo") + } + if !strings.Contains(userinfo, ":") { + if userinfo, err = unescape(userinfo, encodeUserPassword); err != nil { + return nil, "", err + } + user = User(userinfo) + } else { + username, password, _ := strings.Cut(userinfo, ":") + if username, err = unescape(username, encodeUserPassword); err != nil { + return nil, "", err + } + if password, err = unescape(password, encodeUserPassword); err != nil { + return nil, "", err + } + user = UserPassword(username, password) + } + return user, host, nil +} + +// parseHost parses host as an authority without user +// information. That is, as host[:port]. +func parseHost(host string) (string, error) { + if strings.HasPrefix(host, "[") { + // Parse an IP-Literal in RFC 3986 and RFC 6874. + // E.g., "[fe80::1]", "[fe80::1%25en0]", "[fe80::1]:80". + i := strings.LastIndex(host, "]") + if i < 0 { + return "", errors.New("missing ']' in host") + } + colonPort := host[i+1:] + if !validOptionalPort(colonPort) { + return "", fmt.Errorf("invalid port %q after host", colonPort) + } + + // RFC 6874 defines that %25 (%-encoded percent) introduces + // the zone identifier, and the zone identifier can use basically + // any %-encoding it likes. That's different from the host, which + // can only %-encode non-ASCII bytes. + // We do impose some restrictions on the zone, to avoid stupidity + // like newlines. + zone := strings.Index(host[:i], "%25") + if zone >= 0 { + host1, err := unescape(host[:zone], encodeHost) + if err != nil { + return "", err + } + host2, err := unescape(host[zone:i], encodeZone) + if err != nil { + return "", err + } + host3, err := unescape(host[i:], encodeHost) + if err != nil { + return "", err + } + return host1 + host2 + host3, nil + } + } else if i := strings.LastIndex(host, ":"); i != -1 { + colonPort := host[i:] + if !validOptionalPort(colonPort) { + return "", fmt.Errorf("invalid port %q after host", colonPort) + } + } + + var err error + if host, err = unescape(host, encodeHost); err != nil { + return "", err + } + return host, nil +} + +// setPath sets the Path and RawPath fields of the URL based on the provided +// escaped path p. It maintains the invariant that RawPath is only specified +// when it differs from the default encoding of the path. +// For example: +// - setPath("/foo/bar") will set Path="/foo/bar" and RawPath="" +// - setPath("/foo%2fbar") will set Path="/foo/bar" and RawPath="/foo%2fbar" +// setPath will return an error only if the provided path contains an invalid +// escaping. +func (u *URL) setPath(p string) error { + path, err := unescape(p, encodePath) + if err != nil { + return err + } + u.Path = path + if escp := escape(path, encodePath); p == escp { + // Default encoding is fine. + u.RawPath = "" + } else { + u.RawPath = p + } + return nil +} + +// EscapedPath returns the escaped form of u.Path. +// In general there are multiple possible escaped forms of any path. +// EscapedPath returns u.RawPath when it is a valid escaping of u.Path. +// Otherwise EscapedPath ignores u.RawPath and computes an escaped +// form on its own. +// The String and RequestURI methods use EscapedPath to construct +// their results. +// In general, code should call EscapedPath instead of +// reading u.RawPath directly. +func (u *URL) EscapedPath() string { + if u.RawPath != "" && validEncoded(u.RawPath, encodePath) { + p, err := unescape(u.RawPath, encodePath) + if err == nil && p == u.Path { + return u.RawPath + } + } + if u.Path == "*" { + return "*" // don't escape (Issue 11202) + } + return escape(u.Path, encodePath) +} + +// validEncoded reports whether s is a valid encoded path or fragment, +// according to mode. +// It must not contain any bytes that require escaping during encoding. +func validEncoded(s string, mode encoding) bool { + for i := 0; i < len(s); i++ { + // RFC 3986, Appendix A. + // pchar = unreserved / pct-encoded / sub-delims / ":" / "@". + // shouldEscape is not quite compliant with the RFC, + // so we check the sub-delims ourselves and let + // shouldEscape handle the others. + switch s[i] { + case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '@': + // ok + case '[', ']': + // ok - not specified in RFC 3986 but left alone by modern browsers + case '%': + // ok - percent encoded, will decode + default: + if shouldEscape(s[i], mode) { + return false + } + } + } + return true +} + +// setFragment is like setPath but for Fragment/RawFragment. +func (u *URL) setFragment(f string) error { + frag, err := unescape(f, encodeFragment) + if err != nil { + return err + } + u.Fragment = frag + if escf := escape(frag, encodeFragment); f == escf { + // Default encoding is fine. + u.RawFragment = "" + } else { + u.RawFragment = f + } + return nil +} + +// EscapedFragment returns the escaped form of u.Fragment. +// In general there are multiple possible escaped forms of any fragment. +// EscapedFragment returns u.RawFragment when it is a valid escaping of u.Fragment. +// Otherwise EscapedFragment ignores u.RawFragment and computes an escaped +// form on its own. +// The String method uses EscapedFragment to construct its result. +// In general, code should call EscapedFragment instead of +// reading u.RawFragment directly. +func (u *URL) EscapedFragment() string { + if u.RawFragment != "" && validEncoded(u.RawFragment, encodeFragment) { + f, err := unescape(u.RawFragment, encodeFragment) + if err == nil && f == u.Fragment { + return u.RawFragment + } + } + return escape(u.Fragment, encodeFragment) +} + +// validOptionalPort reports whether port is either an empty string +// or matches /^:\d*$/ +func validOptionalPort(port string) bool { + if port == "" { + return true + } + if port[0] != ':' { + return false + } + for _, b := range port[1:] { + if (b < '0' || b > '9') && (b != '-' && b != ',') { + // Neither a digit nor a valid separator character. + return false + } + } + return true +} + +// String reassembles the URL into a valid URL string. +// The general form of the result is one of: +// +// scheme:opaque?query#fragment +// scheme://userinfo@host/path?query#fragment +// +// If u.Opaque is non-empty, String uses the first form; +// otherwise it uses the second form. +// Any non-ASCII characters in host are escaped. +// To obtain the path, String uses u.EscapedPath(). +// +// In the second form, the following rules apply: +// - if u.Scheme is empty, scheme: is omitted. +// - if u.User is nil, userinfo@ is omitted. +// - if u.Host is empty, host/ is omitted. +// - if u.Scheme and u.Host are empty and u.User is nil, +// the entire scheme://userinfo@host/ is omitted. +// - if u.Host is non-empty and u.Path begins with a /, +// the form host/path does not add its own /. +// - if u.RawQuery is empty, ?query is omitted. +// - if u.Fragment is empty, #fragment is omitted. +func (u *URL) String() string { + var buf strings.Builder + if u.Scheme != "" { + buf.WriteString(u.Scheme) + buf.WriteByte(':') + } + if u.Opaque != "" { + buf.WriteString(u.Opaque) + } else { + if u.Scheme != "" || u.Host != "" || u.User != nil { + if u.OmitHost && u.Host == "" && u.User == nil { + // omit empty host + } else { + if u.Host != "" || u.Path != "" || u.User != nil { + buf.WriteString("//") + } + if ui := u.User; ui != nil { + buf.WriteString(ui.String()) + buf.WriteByte('@') + } + if h := u.Host; h != "" { + buf.WriteString(escape(h, encodeHost)) + } + } + } + path := u.EscapedPath() + if path != "" && path[0] != '/' && u.Host != "" { + buf.WriteByte('/') + } + if buf.Len() == 0 { + // RFC 3986 §4.2 + // A path segment that contains a colon character (e.g., "this:that") + // cannot be used as the first segment of a relative-path reference, as + // it would be mistaken for a scheme name. Such a segment must be + // preceded by a dot-segment (e.g., "./this:that") to make a relative- + // path reference. + if segment, _, _ := strings.Cut(path, "/"); strings.Contains(segment, ":") { + buf.WriteString("./") + } + } + buf.WriteString(path) + } + if u.ForceQuery || u.RawQuery != "" { + buf.WriteByte('?') + buf.WriteString(u.RawQuery) + } + if u.Fragment != "" { + buf.WriteByte('#') + buf.WriteString(u.EscapedFragment()) + } + return buf.String() +} + +// Redacted is like String but replaces any password with "xxxxx". +// Only the password in u.User is redacted. +func (u *URL) Redacted() string { + if u == nil { + return "" + } + + ru := *u + if _, has := ru.User.Password(); has { + ru.User = UserPassword(ru.User.Username(), "xxxxx") + } + return ru.String() +} + +// Values maps a string key to a list of values. +// It is typically used for query parameters and form values. +// Unlike in the http.Header map, the keys in a Values map +// are case-sensitive. +type Values map[string][]string + +// Get gets the first value associated with the given key. +// If there are no values associated with the key, Get returns +// the empty string. To access multiple values, use the map +// directly. +func (v Values) Get(key string) string { + vs := v[key] + if len(vs) == 0 { + return "" + } + return vs[0] +} + +// Set sets the key to value. It replaces any existing +// values. +func (v Values) Set(key, value string) { + v[key] = []string{value} +} + +// Add adds the value to key. It appends to any existing +// values associated with key. +func (v Values) Add(key, value string) { + v[key] = append(v[key], value) +} + +// Del deletes the values associated with key. +func (v Values) Del(key string) { + delete(v, key) +} + +// Has checks whether a given key is set. +func (v Values) Has(key string) bool { + _, ok := v[key] + return ok +} + +// ParseQuery parses the URL-encoded query string and returns +// a map listing the values specified for each key. +// ParseQuery always returns a non-nil map containing all the +// valid query parameters found; err describes the first decoding error +// encountered, if any. +// +// Query is expected to be a list of key=value settings separated by ampersands. +// A setting without an equals sign is interpreted as a key set to an empty +// value. +// Settings containing a non-URL-encoded semicolon are considered invalid. +func ParseQuery(query string) (Values, error) { + m := make(Values) + err := parseQuery(m, query) + return m, err +} + +func parseQuery(m Values, query string) (err error) { + for query != "" { + var key string + key, query, _ = strings.Cut(query, "&") + if strings.Contains(key, ";") { + err = fmt.Errorf("invalid semicolon separator in query") + continue + } + if key == "" { + continue + } + key, value, _ := strings.Cut(key, "=") + key, err1 := QueryUnescape(key) + if err1 != nil { + if err == nil { + err = err1 + } + continue + } + value, err1 = QueryUnescape(value) + if err1 != nil { + if err == nil { + err = err1 + } + continue + } + m[key] = append(m[key], value) + } + return err +} + +// Encode encodes the values into “URL encoded” form +// ("bar=baz&foo=quux") sorted by key. +func (v Values) Encode() string { + if v == nil { + return "" + } + var buf strings.Builder + keys := make([]string, 0, len(v)) + for k := range v { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + vs := v[k] + keyEscaped := QueryEscape(k) + for _, v := range vs { + if buf.Len() > 0 { + buf.WriteByte('&') + } + buf.WriteString(keyEscaped) + buf.WriteByte('=') + buf.WriteString(QueryEscape(v)) + } + } + return buf.String() +} + +// resolvePath applies special path segments from refs and applies +// them to base, per RFC 3986. +func resolvePath(base, ref string) string { + var full string + if ref == "" { + full = base + } else if ref[0] != '/' { + i := strings.LastIndex(base, "/") + full = base[:i+1] + ref + } else { + full = ref + } + if full == "" { + return "" + } + + var ( + elem string + dst strings.Builder + ) + first := true + remaining := full + // We want to return a leading '/', so write it now. + dst.WriteByte('/') + found := true + for found { + elem, remaining, found = strings.Cut(remaining, "/") + if elem == "." { + first = false + // drop + continue + } + + if elem == ".." { + // Ignore the leading '/' we already wrote. + str := dst.String()[1:] + index := strings.LastIndexByte(str, '/') + + dst.Reset() + dst.WriteByte('/') + if index == -1 { + first = true + } else { + dst.WriteString(str[:index]) + } + } else { + if !first { + dst.WriteByte('/') + } + dst.WriteString(elem) + first = false + } + } + + if elem == "." || elem == ".." { + dst.WriteByte('/') + } + + // We wrote an initial '/', but we don't want two. + r := dst.String() + if len(r) > 1 && r[1] == '/' { + r = r[1:] + } + return r +} + +// IsAbs reports whether the URL is absolute. +// Absolute means that it has a non-empty scheme. +func (u *URL) IsAbs() bool { + return u.Scheme != "" +} + +// Parse parses a URL in the context of the receiver. The provided URL +// may be relative or absolute. Parse returns nil, err on parse +// failure, otherwise its return value is the same as ResolveReference. +func (u *URL) Parse(ref string) (*URL, error) { + refURL, err := Parse(ref) + if err != nil { + return nil, err + } + return u.ResolveReference(refURL), nil +} + +// ResolveReference resolves a URI reference to an absolute URI from +// an absolute base URI u, per RFC 3986 Section 5.2. The URI reference +// may be relative or absolute. ResolveReference always returns a new +// URL instance, even if the returned URL is identical to either the +// base or reference. If ref is an absolute URL, then ResolveReference +// ignores base and returns a copy of ref. +func (u *URL) ResolveReference(ref *URL) *URL { + url := *ref + if ref.Scheme == "" { + url.Scheme = u.Scheme + } + if ref.Scheme != "" || ref.Host != "" || ref.User != nil { + // The "absoluteURI" or "net_path" cases. + // We can ignore the error from setPath since we know we provided a + // validly-escaped path. + url.setPath(resolvePath(ref.EscapedPath(), "")) + return &url + } + if ref.Opaque != "" { + url.User = nil + url.Host = "" + url.Path = "" + return &url + } + if ref.Path == "" && !ref.ForceQuery && ref.RawQuery == "" { + url.RawQuery = u.RawQuery + if ref.Fragment == "" { + url.Fragment = u.Fragment + url.RawFragment = u.RawFragment + } + } + // The "abs_path" or "rel_path" cases. + url.Host = u.Host + url.User = u.User + url.setPath(resolvePath(u.EscapedPath(), ref.EscapedPath())) + return &url +} + +// Query parses RawQuery and returns the corresponding values. +// It silently discards malformed value pairs. +// To check errors use ParseQuery. +func (u *URL) Query() Values { + v, _ := ParseQuery(u.RawQuery) + return v +} + +// RequestURI returns the encoded path?query or opaque?query +// string that would be used in an HTTP request for u. +func (u *URL) RequestURI() string { + result := u.Opaque + if result == "" { + result = u.EscapedPath() + if result == "" { + result = "/" + } + } else { + if strings.HasPrefix(result, "//") { + result = u.Scheme + ":" + result + } + } + if u.ForceQuery || u.RawQuery != "" { + result += "?" + u.RawQuery + } + return result +} + +// Hostname returns u.Host, stripping any valid port number if present. +// +// If the result is enclosed in square brackets, as literal IPv6 addresses are, +// the square brackets are removed from the result. +func (u *URL) Hostname() string { + host, _ := splitHostPort(u.Host) + return host +} + +// Port returns the port part of u.Host, without the leading colon. +// +// If u.Host doesn't contain a valid numeric port, Port returns an empty string. +func (u *URL) Port() string { + _, port := splitHostPort(u.Host) + return port +} + +// splitHostPort separates host and port. If the port is not valid, it returns +// the entire input as host, and it doesn't check the validity of the host. +// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric. +func splitHostPort(hostPort string) (host, port string) { + host = hostPort + + colon := strings.LastIndexByte(host, ':') + if colon != -1 && validOptionalPort(host[colon:]) { + host, port = host[:colon], host[colon+1:] + } + + if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { + host = host[1 : len(host)-1] + } + + return +} + +// Marshaling interface implementations. +// Would like to implement MarshalText/UnmarshalText but that will change the JSON representation of URLs. + +func (u *URL) MarshalBinary() (text []byte, err error) { + return []byte(u.String()), nil +} + +func (u *URL) UnmarshalBinary(text []byte) error { + u1, err := Parse(string(text)) + if err != nil { + return err + } + *u = *u1 + return nil +} + +// JoinPath returns a new URL with the provided path elements joined to +// any existing path and the resulting path cleaned of any ./ or ../ elements. +// Any sequences of multiple / characters will be reduced to a single /. +func (u *URL) JoinPath(elem ...string) *URL { + elem = append([]string{u.EscapedPath()}, elem...) + var p string + if !strings.HasPrefix(elem[0], "/") { + // Return a relative path if u is relative, + // but ensure that it contains no ../ elements. + elem[0] = "/" + elem[0] + p = path.Join(elem...)[1:] + } else { + p = path.Join(elem...) + } + // path.Join will remove any trailing slashes. + // Preserve at least one. + if strings.HasSuffix(elem[len(elem)-1], "/") && !strings.HasSuffix(p, "/") { + p += "/" + } + url := *u + url.setPath(p) + return &url +} + +// validUserinfo reports whether s is a valid userinfo string per RFC 3986 +// Section 3.2.1: +// +// userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) +// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" +// / "*" / "+" / "," / ";" / "=" +// +// It doesn't validate pct-encoded. The caller does that via func unescape. +func validUserinfo(s string) bool { + for _, r := range s { + if 'A' <= r && r <= 'Z' { + continue + } + if 'a' <= r && r <= 'z' { + continue + } + if '0' <= r && r <= '9' { + continue + } + switch r { + case '-', '.', '_', ':', '~', '!', '$', '&', '\'', + '(', ')', '*', '+', ',', ';', '=', '%', '@': + continue + default: + return false + } + } + return true +} + +// stringContainsCTLByte reports whether s contains any ASCII control character. +func stringContainsCTLByte(s string) bool { + for i := 0; i < len(s); i++ { + b := s[i] + if b < ' ' || b == 0x7f { + return true + } + } + return false +} + +// JoinPath returns a URL string with the provided path elements joined to +// the existing path of base and the resulting path cleaned of any ./ or ../ elements. +func JoinPath(base string, elem ...string) (result string, err error) { + url, err := Parse(base) + if err != nil { + return + } + result = url.JoinPath(elem...).String() + return +} diff --git a/v2rayng/hysteria/app/internal/url/url_test.go b/v2rayng/hysteria/app/internal/url/url_test.go new file mode 100644 index 0000000000..effec5c55e --- /dev/null +++ b/v2rayng/hysteria/app/internal/url/url_test.go @@ -0,0 +1,91 @@ +package url + +import ( + "reflect" + "testing" +) + +func TestParse(t *testing.T) { + type args struct { + rawURL string + } + tests := []struct { + name string + args args + want *URL + wantErr bool + }{ + { + name: "no port", + args: args{ + rawURL: "hysteria2://ganggang@icecreamsogood/", + }, + want: &URL{ + Scheme: "hysteria2", + User: User("ganggang"), + Host: "icecreamsogood", + Path: "/", + }, + }, + { + name: "single port", + args: args{ + rawURL: "hysteria2://yesyes@icecreamsogood:8888/", + }, + want: &URL{ + Scheme: "hysteria2", + User: User("yesyes"), + Host: "icecreamsogood:8888", + Path: "/", + }, + }, + { + name: "multi port", + args: args{ + rawURL: "hysteria2://darkness@laplus.org:8888,9999,11111/", + }, + want: &URL{ + Scheme: "hysteria2", + User: User("darkness"), + Host: "laplus.org:8888,9999,11111", + Path: "/", + }, + }, + { + name: "range port", + args: args{ + rawURL: "hysteria2://darkness@laplus.org:8888-9999/", + }, + want: &URL{ + Scheme: "hysteria2", + User: User("darkness"), + Host: "laplus.org:8888-9999", + Path: "/", + }, + }, + { + name: "both", + args: args{ + rawURL: "hysteria2://gawr:gura@atlantis.moe:443,7788-8899,10010/", + }, + want: &URL{ + Scheme: "hysteria2", + User: UserPassword("gawr", "gura"), + Host: "atlantis.moe:443,7788-8899,10010", + Path: "/", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Parse(tt.args.rawURL) + if (err != nil) != tt.wantErr { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Parse() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/v2rayng/hysteria/app/internal/utils/bpsconv.go b/v2rayng/hysteria/app/internal/utils/bpsconv.go new file mode 100644 index 0000000000..7cad55805d --- /dev/null +++ b/v2rayng/hysteria/app/internal/utils/bpsconv.go @@ -0,0 +1,68 @@ +package utils + +import ( + "errors" + "fmt" + "strconv" + "strings" +) + +const ( + Byte = 1 + Kilobyte = Byte * 1000 + Megabyte = Kilobyte * 1000 + Gigabyte = Megabyte * 1000 + Terabyte = Gigabyte * 1000 +) + +// StringToBps converts a string to a bandwidth value in bytes per second. +// E.g. "100 Mbps", "512 kbps", "1g" are all valid. +func StringToBps(s string) (uint64, error) { + s = strings.ToLower(strings.TrimSpace(s)) + spl := 0 + for i, c := range s { + if c < '0' || c > '9' { + spl = i + break + } + } + if spl == 0 { + // No unit or no value + return 0, errors.New("invalid format") + } + v, err := strconv.ParseUint(s[:spl], 10, 64) + if err != nil { + return 0, err + } + unit := strings.TrimSpace(s[spl:]) + + switch strings.ToLower(unit) { + case "b", "bps": + return v * Byte / 8, nil + case "k", "kb", "kbps": + return v * Kilobyte / 8, nil + case "m", "mb", "mbps": + return v * Megabyte / 8, nil + case "g", "gb", "gbps": + return v * Gigabyte / 8, nil + case "t", "tb", "tbps": + return v * Terabyte / 8, nil + default: + return 0, errors.New("unsupported unit") + } +} + +// ConvBandwidth handles both string and int types for bandwidth. +// When using string, it will be parsed as a bandwidth string with units. +// When using int, it will be parsed as a raw bandwidth in bytes per second. +// It does NOT support float types. +func ConvBandwidth(bw interface{}) (uint64, error) { + switch bwT := bw.(type) { + case string: + return StringToBps(bwT) + case int: + return uint64(bwT), nil + default: + return 0, fmt.Errorf("invalid type %T for bandwidth", bwT) + } +} diff --git a/v2rayng/hysteria/app/internal/utils/bpsconv_test.go b/v2rayng/hysteria/app/internal/utils/bpsconv_test.go new file mode 100644 index 0000000000..32265605a0 --- /dev/null +++ b/v2rayng/hysteria/app/internal/utils/bpsconv_test.go @@ -0,0 +1,40 @@ +package utils + +import "testing" + +func TestStringToBps(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + want uint64 + wantErr bool + }{ + {"bps", args{"800 bps"}, 100, false}, + {"kbps", args{"800 kbps"}, 100_000, false}, + {"mbps", args{"800 mbps"}, 100_000_000, false}, + {"gbps", args{"800 gbps"}, 100_000_000_000, false}, + {"tbps", args{"800 tbps"}, 100_000_000_000_000, false}, + {"mbps simp", args{"100m"}, 12_500_000, false}, + {"gbps simp upper", args{"2G"}, 250_000_000, false}, + {"invalid 1", args{"damn"}, 0, true}, + {"invalid 2", args{"6444"}, 0, true}, + {"invalid 3", args{"5.4 mbps"}, 0, true}, + {"invalid 4", args{"kbps"}, 0, true}, + {"invalid 5", args{"1234 5678 gbps"}, 0, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := StringToBps(tt.args.s) + if (err != nil) != tt.wantErr { + t.Errorf("StringToBps() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("StringToBps() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/v2rayng/hysteria/app/internal/utils/certloader.go b/v2rayng/hysteria/app/internal/utils/certloader.go new file mode 100644 index 0000000000..fb41a3c57e --- /dev/null +++ b/v2rayng/hysteria/app/internal/utils/certloader.go @@ -0,0 +1,198 @@ +package utils + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "net" + "os" + "strings" + "sync" + "sync/atomic" + "time" +) + +type LocalCertificateLoader struct { + CertFile string + KeyFile string + SNIGuard SNIGuardFunc + + lock sync.Mutex + cache atomic.Pointer[localCertificateCache] +} + +type SNIGuardFunc func(info *tls.ClientHelloInfo, cert *tls.Certificate) error + +// localCertificateCache holds the certificate and its mod times. +// this struct is designed to be read-only. +// +// to update the cache, use LocalCertificateLoader.makeCache and +// update the LocalCertificateLoader.cache field. +type localCertificateCache struct { + certificate *tls.Certificate + certModTime time.Time + keyModTime time.Time +} + +func (l *LocalCertificateLoader) InitializeCache() error { + l.lock.Lock() + defer l.lock.Unlock() + + cache, err := l.makeCache() + if err != nil { + return err + } + + l.cache.Store(cache) + return nil +} + +func (l *LocalCertificateLoader) GetCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) { + cert, err := l.getCertificateWithCache() + if err != nil { + return nil, err + } + + if l.SNIGuard == nil { + return cert, nil + } + err = l.SNIGuard(info, cert) + if err != nil { + return nil, err + } + + return cert, nil +} + +func (l *LocalCertificateLoader) checkModTime() (certModTime, keyModTime time.Time, err error) { + fi, err := os.Stat(l.CertFile) + if err != nil { + err = fmt.Errorf("failed to stat certificate file: %w", err) + return + } + certModTime = fi.ModTime() + + fi, err = os.Stat(l.KeyFile) + if err != nil { + err = fmt.Errorf("failed to stat key file: %w", err) + return + } + keyModTime = fi.ModTime() + return +} + +func (l *LocalCertificateLoader) makeCache() (cache *localCertificateCache, err error) { + c := &localCertificateCache{} + + c.certModTime, c.keyModTime, err = l.checkModTime() + if err != nil { + return + } + + cert, err := tls.LoadX509KeyPair(l.CertFile, l.KeyFile) + if err != nil { + return + } + c.certificate = &cert + if c.certificate.Leaf == nil { + // certificate.Leaf was left nil by tls.LoadX509KeyPair before Go 1.23 + c.certificate.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + return + } + } + + cache = c + return +} + +func (l *LocalCertificateLoader) getCertificateWithCache() (*tls.Certificate, error) { + cache := l.cache.Load() + + certModTime, keyModTime, terr := l.checkModTime() + if terr != nil { + if cache != nil { + // use cache when file is temporarily unavailable + return cache.certificate, nil + } + return nil, terr + } + + if cache != nil && cache.certModTime.Equal(certModTime) && cache.keyModTime.Equal(keyModTime) { + // cache is up-to-date + return cache.certificate, nil + } + + if cache != nil { + if !l.lock.TryLock() { + // another goroutine is updating the cache + return cache.certificate, nil + } + } else { + l.lock.Lock() + } + defer l.lock.Unlock() + + if l.cache.Load() != cache { + // another goroutine updated the cache + return l.cache.Load().certificate, nil + } + + newCache, err := l.makeCache() + if err != nil { + if cache != nil { + // use cache when loading failed + return cache.certificate, nil + } + return nil, err + } + + l.cache.Store(newCache) + return newCache.certificate, nil +} + +// getNameFromClientHello returns a normalized form of hello.ServerName. +// If hello.ServerName is empty (i.e. client did not use SNI), then the +// associated connection's local address is used to extract an IP address. +// +// ref: https://github.com/caddyserver/certmagic/blob/3bad5b6bb595b09c14bd86ff0b365d302faaf5e2/handshake.go#L838 +func getNameFromClientHello(hello *tls.ClientHelloInfo) string { + normalizedName := func(serverName string) string { + return strings.ToLower(strings.TrimSpace(serverName)) + } + localIPFromConn := func(c net.Conn) string { + if c == nil { + return "" + } + localAddr := c.LocalAddr().String() + ip, _, err := net.SplitHostPort(localAddr) + if err != nil { + ip = localAddr + } + if scopeIDStart := strings.Index(ip, "%"); scopeIDStart > -1 { + ip = ip[:scopeIDStart] + } + return ip + } + + if name := normalizedName(hello.ServerName); name != "" { + return name + } + return localIPFromConn(hello.Conn) +} + +func SNIGuardDNSSAN(info *tls.ClientHelloInfo, cert *tls.Certificate) error { + if len(cert.Leaf.DNSNames) == 0 { + return nil + } + return SNIGuardStrict(info, cert) +} + +func SNIGuardStrict(info *tls.ClientHelloInfo, cert *tls.Certificate) error { + hostname := getNameFromClientHello(info) + err := cert.Leaf.VerifyHostname(hostname) + if err != nil { + return fmt.Errorf("sni guard: %w", err) + } + return nil +} diff --git a/v2rayng/hysteria/app/internal/utils/certloader_test.go b/v2rayng/hysteria/app/internal/utils/certloader_test.go new file mode 100644 index 0000000000..3a8e26bff8 --- /dev/null +++ b/v2rayng/hysteria/app/internal/utils/certloader_test.go @@ -0,0 +1,139 @@ +package utils + +import ( + "crypto/tls" + "log" + "net/http" + "os" + "os/exec" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + testListen = "127.82.39.147:12947" + testCAFile = "./testcerts/ca" + testCertFile = "./testcerts/cert" + testKeyFile = "./testcerts/key" +) + +func TestCertificateLoaderPathError(t *testing.T) { + assert.NoError(t, os.RemoveAll(testCertFile)) + assert.NoError(t, os.RemoveAll(testKeyFile)) + loader := LocalCertificateLoader{ + CertFile: testCertFile, + KeyFile: testKeyFile, + SNIGuard: SNIGuardStrict, + } + err := loader.InitializeCache() + var pathErr *os.PathError + assert.ErrorAs(t, err, &pathErr) +} + +func TestCertificateLoaderFullChain(t *testing.T) { + assert.NoError(t, generateTestCertificate([]string{"example.com"}, "fullchain")) + + loader := LocalCertificateLoader{ + CertFile: testCertFile, + KeyFile: testKeyFile, + SNIGuard: SNIGuardStrict, + } + assert.NoError(t, loader.InitializeCache()) + + lis, err := tls.Listen("tcp", testListen, &tls.Config{ + GetCertificate: loader.GetCertificate, + }) + assert.NoError(t, err) + defer lis.Close() + go http.Serve(lis, nil) + + assert.Error(t, runTestTLSClient("unmatched-sni.example.com")) + assert.Error(t, runTestTLSClient("")) + assert.NoError(t, runTestTLSClient("example.com")) +} + +func TestCertificateLoaderNoSAN(t *testing.T) { + assert.NoError(t, generateTestCertificate(nil, "selfsign")) + + loader := LocalCertificateLoader{ + CertFile: testCertFile, + KeyFile: testKeyFile, + SNIGuard: SNIGuardDNSSAN, + } + assert.NoError(t, loader.InitializeCache()) + + lis, err := tls.Listen("tcp", testListen, &tls.Config{ + GetCertificate: loader.GetCertificate, + }) + assert.NoError(t, err) + defer lis.Close() + go http.Serve(lis, nil) + + assert.NoError(t, runTestTLSClient("")) +} + +func TestCertificateLoaderReplaceCertificate(t *testing.T) { + assert.NoError(t, generateTestCertificate([]string{"example.com"}, "fullchain")) + + loader := LocalCertificateLoader{ + CertFile: testCertFile, + KeyFile: testKeyFile, + SNIGuard: SNIGuardStrict, + } + assert.NoError(t, loader.InitializeCache()) + + lis, err := tls.Listen("tcp", testListen, &tls.Config{ + GetCertificate: loader.GetCertificate, + }) + assert.NoError(t, err) + defer lis.Close() + go http.Serve(lis, nil) + + assert.NoError(t, runTestTLSClient("example.com")) + assert.Error(t, runTestTLSClient("2.example.com")) + + assert.NoError(t, generateTestCertificate([]string{"2.example.com"}, "fullchain")) + + assert.Error(t, runTestTLSClient("example.com")) + assert.NoError(t, runTestTLSClient("2.example.com")) +} + +func generateTestCertificate(dnssan []string, certType string) error { + args := []string{ + "certloader_test_gencert.py", + "--ca", testCAFile, + "--cert", testCertFile, + "--key", testKeyFile, + "--type", certType, + } + if len(dnssan) > 0 { + args = append(args, "--dnssan", strings.Join(dnssan, ",")) + } + cmd := exec.Command("python", args...) + out, err := cmd.CombinedOutput() + if err != nil { + log.Printf("Failed to generate test certificate: %s", out) + return err + } + return nil +} + +func runTestTLSClient(sni string) error { + args := []string{ + "certloader_test_tlsclient.py", + "--server", testListen, + "--ca", testCAFile, + } + if sni != "" { + args = append(args, "--sni", sni) + } + cmd := exec.Command("python", args...) + out, err := cmd.CombinedOutput() + if err != nil { + log.Printf("Failed to run test TLS client: %s", out) + return err + } + return nil +} diff --git a/v2rayng/hysteria/app/internal/utils/certloader_test_gencert.py b/v2rayng/hysteria/app/internal/utils/certloader_test_gencert.py new file mode 100644 index 0000000000..d4d569598c --- /dev/null +++ b/v2rayng/hysteria/app/internal/utils/certloader_test_gencert.py @@ -0,0 +1,134 @@ +import argparse +import datetime +from cryptography import x509 +from cryptography.x509.oid import NameOID +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption + + +def create_key(): + return ec.generate_private_key(ec.SECP256R1()) + + +def create_certificate(cert_type, subject, issuer, private_key, public_key, dns_san=None): + serial_number = x509.random_serial_number() + not_valid_before = datetime.datetime.now(datetime.UTC) + not_valid_after = not_valid_before + datetime.timedelta(days=365) + + subject_name = x509.Name([ + x509.NameAttribute(NameOID.COUNTRY_NAME, subject.get('C', 'ZZ')), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, subject.get('O', 'No Organization')), + x509.NameAttribute(NameOID.COMMON_NAME, subject.get('CN', 'No CommonName')), + ]) + issuer_name = x509.Name([ + x509.NameAttribute(NameOID.COUNTRY_NAME, issuer.get('C', 'ZZ')), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, issuer.get('O', 'No Organization')), + x509.NameAttribute(NameOID.COMMON_NAME, issuer.get('CN', 'No CommonName')), + ]) + builder = x509.CertificateBuilder() + builder = builder.subject_name(subject_name) + builder = builder.issuer_name(issuer_name) + builder = builder.public_key(public_key) + builder = builder.serial_number(serial_number) + builder = builder.not_valid_before(not_valid_before) + builder = builder.not_valid_after(not_valid_after) + if cert_type == 'root': + builder = builder.add_extension( + x509.BasicConstraints(ca=True, path_length=None), critical=True + ) + elif cert_type == 'intermediate': + builder = builder.add_extension( + x509.BasicConstraints(ca=True, path_length=0), critical=True + ) + elif cert_type == 'leaf': + builder = builder.add_extension( + x509.BasicConstraints(ca=False, path_length=None), critical=True + ) + else: + raise ValueError(f'Invalid cert_type: {cert_type}') + if dns_san: + builder = builder.add_extension( + x509.SubjectAlternativeName([x509.DNSName(d) for d in dns_san.split(',')]), + critical=False + ) + return builder.sign(private_key=private_key, algorithm=hashes.SHA256()) + + +def main(): + parser = argparse.ArgumentParser(description='Generate HTTPS server certificate.') + parser.add_argument('--ca', required=True, + help='Path to write the X509 CA certificate in PEM format') + parser.add_argument('--cert', required=True, + help='Path to write the X509 certificate in PEM format') + parser.add_argument('--key', required=True, + help='Path to write the private key in PEM format') + parser.add_argument('--dnssan', required=False, default=None, + help='Comma-separated list of DNS SANs') + parser.add_argument('--type', required=True, choices=['selfsign', 'fullchain'], + help='Type of certificate to generate') + + args = parser.parse_args() + + key = create_key() + public_key = key.public_key() + + if args.type == 'selfsign': + subject = {"C": "ZZ", "O": "Certificate", "CN": "Certificate"} + cert = create_certificate( + cert_type='root', + subject=subject, + issuer=subject, + private_key=key, + public_key=public_key, + dns_san=args.dnssan) + with open(args.ca, 'wb') as f: + f.write(cert.public_bytes(Encoding.PEM)) + with open(args.cert, 'wb') as f: + f.write(cert.public_bytes(Encoding.PEM)) + with open(args.key, 'wb') as f: + f.write( + key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption())) + + elif args.type == 'fullchain': + ca_key = create_key() + ca_public_key = ca_key.public_key() + ca_subject = {"C": "ZZ", "O": "Root CA", "CN": "Root CA"} + ca_cert = create_certificate( + cert_type='root', + subject=ca_subject, + issuer=ca_subject, + private_key=ca_key, + public_key=ca_public_key) + + intermediate_key = create_key() + intermediate_public_key = intermediate_key.public_key() + intermediate_subject = {"C": "ZZ", "O": "Intermediate CA", "CN": "Intermediate CA"} + intermediate_cert = create_certificate( + cert_type='intermediate', + subject=intermediate_subject, + issuer=ca_subject, + private_key=ca_key, + public_key=intermediate_public_key) + + leaf_subject = {"C": "ZZ", "O": "Leaf Certificate", "CN": "Leaf Certificate"} + cert = create_certificate( + cert_type='leaf', + subject=leaf_subject, + issuer=intermediate_subject, + private_key=intermediate_key, + public_key=public_key, + dns_san=args.dnssan) + + with open(args.ca, 'wb') as f: + f.write(ca_cert.public_bytes(Encoding.PEM)) + with open(args.cert, 'wb') as f: + f.write(cert.public_bytes(Encoding.PEM)) + f.write(intermediate_cert.public_bytes(Encoding.PEM)) + with open(args.key, 'wb') as f: + f.write( + key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption())) + + +if __name__ == "__main__": + main() diff --git a/v2rayng/hysteria/app/internal/utils/certloader_test_tlsclient.py b/v2rayng/hysteria/app/internal/utils/certloader_test_tlsclient.py new file mode 100644 index 0000000000..3b7efd63b5 --- /dev/null +++ b/v2rayng/hysteria/app/internal/utils/certloader_test_tlsclient.py @@ -0,0 +1,60 @@ +import argparse +import ssl +import socket +import sys + + +def check_tls(server, ca_cert, sni, alpn): + try: + host, port = server.split(":") + port = int(port) + + if ca_cert: + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=ca_cert) + context.check_hostname = sni is not None + context.verify_mode = ssl.CERT_REQUIRED + else: + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + if alpn: + context.set_alpn_protocols([p for p in alpn.split(",")]) + + with socket.create_connection((host, port)) as sock: + with context.wrap_socket(sock, server_hostname=sni) as ssock: + # Verify handshake and certificate + print(f'Connected to {ssock.version()} using {ssock.cipher()}') + print(f'Server certificate validated and details: {ssock.getpeercert()}') + print("OK") + return 0 + except Exception as e: + print(f"Error: {e}") + return 1 + + +def main(): + parser = argparse.ArgumentParser(description="Test TLS Server") + parser.add_argument("--server", required=True, + help="Server address to test (e.g., 127.1.2.3:8443)") + parser.add_argument("--ca", required=False, default=None, + help="CA certificate file used to validate the server certificate" + "Omit to use insecure connection") + parser.add_argument("--sni", required=False, default=None, + help="SNI to send in ClientHello") + parser.add_argument("--alpn", required=False, default='h2', + help="ALPN to send in ClientHello") + + args = parser.parse_args() + + exit_status = check_tls( + server=args.server, + ca_cert=args.ca, + sni=args.sni, + alpn=args.alpn) + + sys.exit(exit_status) + + +if __name__ == "__main__": + main() diff --git a/v2rayng/hysteria/app/internal/utils/geoloader.go b/v2rayng/hysteria/app/internal/utils/geoloader.go new file mode 100644 index 0000000000..468c68ab6d --- /dev/null +++ b/v2rayng/hysteria/app/internal/utils/geoloader.go @@ -0,0 +1,172 @@ +package utils + +import ( + "fmt" + "io" + "net/http" + "os" + "time" + + "github.com/apernet/hysteria/extras/v2/outbounds/acl" + "github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo" +) + +const ( + geoipFilename = "geoip.dat" + geoipURL = "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat" + geositeFilename = "geosite.dat" + geositeURL = "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat" + geoDlTmpPattern = ".hysteria-geoloader.dlpart.*" + + geoDefaultUpdateInterval = 7 * 24 * time.Hour // 7 days +) + +var _ acl.GeoLoader = (*GeoLoader)(nil) + +// GeoLoader provides the on-demand GeoIP/GeoSite database +// loading functionality required by the ACL engine. +// Empty filenames = automatic download from built-in URLs. +type GeoLoader struct { + GeoIPFilename string + GeoSiteFilename string + UpdateInterval time.Duration + + DownloadFunc func(filename, url string) + DownloadErrFunc func(err error) + + geoipMap map[string]*v2geo.GeoIP + geositeMap map[string]*v2geo.GeoSite +} + +func (l *GeoLoader) shouldDownload(filename string) bool { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return true + } + if info.Size() == 0 { + // empty files are loadable by v2geo, but we consider it broken + return true + } + dt := time.Now().Sub(info.ModTime()) + if l.UpdateInterval == 0 { + return dt > geoDefaultUpdateInterval + } else { + return dt > l.UpdateInterval + } +} + +func (l *GeoLoader) downloadAndCheck(filename, url string, checkFunc func(filename string) error) error { + l.DownloadFunc(filename, url) + + resp, err := http.Get(url) + if err != nil { + l.DownloadErrFunc(err) + return err + } + defer resp.Body.Close() + + f, err := os.CreateTemp(".", geoDlTmpPattern) + if err != nil { + l.DownloadErrFunc(err) + return err + } + defer os.Remove(f.Name()) + + _, err = io.Copy(f, resp.Body) + if err != nil { + f.Close() + l.DownloadErrFunc(err) + return err + } + f.Close() + + err = checkFunc(f.Name()) + if err != nil { + l.DownloadErrFunc(fmt.Errorf("integrity check failed: %w", err)) + return err + } + + err = os.Rename(f.Name(), filename) + if err != nil { + l.DownloadErrFunc(fmt.Errorf("rename failed: %w", err)) + return err + } + + return nil +} + +func (l *GeoLoader) LoadGeoIP() (map[string]*v2geo.GeoIP, error) { + if l.geoipMap != nil { + return l.geoipMap, nil + } + autoDL := false + filename := l.GeoIPFilename + if filename == "" { + autoDL = true + filename = geoipFilename + } + if autoDL { + if !l.shouldDownload(filename) { + m, err := v2geo.LoadGeoIP(filename) + if err == nil { + l.geoipMap = m + return m, nil + } + // file is broken, download it again + } + err := l.downloadAndCheck(filename, geoipURL, func(filename string) error { + _, err := v2geo.LoadGeoIP(filename) + return err + }) + if err != nil { + // as long as the previous download exists, fallback to it + if _, serr := os.Stat(filename); os.IsNotExist(serr) { + return nil, err + } + } + } + m, err := v2geo.LoadGeoIP(filename) + if err != nil { + return nil, err + } + l.geoipMap = m + return m, nil +} + +func (l *GeoLoader) LoadGeoSite() (map[string]*v2geo.GeoSite, error) { + if l.geositeMap != nil { + return l.geositeMap, nil + } + autoDL := false + filename := l.GeoSiteFilename + if filename == "" { + autoDL = true + filename = geositeFilename + } + if autoDL { + if !l.shouldDownload(filename) { + m, err := v2geo.LoadGeoSite(filename) + if err == nil { + l.geositeMap = m + return m, nil + } + // file is broken, download it again + } + err := l.downloadAndCheck(filename, geositeURL, func(filename string) error { + _, err := v2geo.LoadGeoSite(filename) + return err + }) + if err != nil { + // as long as the previous download exists, fallback to it + if _, serr := os.Stat(filename); os.IsNotExist(serr) { + return nil, err + } + } + } + m, err := v2geo.LoadGeoSite(filename) + if err != nil { + return nil, err + } + l.geositeMap = m + return m, nil +} diff --git a/v2rayng/hysteria/app/internal/utils/qr.go b/v2rayng/hysteria/app/internal/utils/qr.go new file mode 100644 index 0000000000..f0c1d394d7 --- /dev/null +++ b/v2rayng/hysteria/app/internal/utils/qr.go @@ -0,0 +1,16 @@ +package utils + +import ( + "os" + + "github.com/mdp/qrterminal/v3" +) + +func PrintQR(str string) { + qrterminal.GenerateWithConfig(str, qrterminal.Config{ + Level: qrterminal.L, + Writer: os.Stdout, + BlackChar: qrterminal.BLACK, + WhiteChar: qrterminal.WHITE, + }) +} diff --git a/v2rayng/hysteria/app/internal/utils/testcerts/.gitignore b/v2rayng/hysteria/app/internal/utils/testcerts/.gitignore new file mode 100644 index 0000000000..082821a007 --- /dev/null +++ b/v2rayng/hysteria/app/internal/utils/testcerts/.gitignore @@ -0,0 +1,3 @@ +# This directory is used for certificate generation in certloader_test.go +/* +!/.gitignore diff --git a/v2rayng/hysteria/app/internal/utils/update.go b/v2rayng/hysteria/app/internal/utils/update.go new file mode 100644 index 0000000000..60da91f4f5 --- /dev/null +++ b/v2rayng/hysteria/app/internal/utils/update.go @@ -0,0 +1,96 @@ +package utils + +import ( + "context" + "encoding/json" + "fmt" + "net" + "net/http" + "time" + + "github.com/apernet/hysteria/core/v2/client" +) + +const ( + updateCheckEndpoint = "https://api.hy2.io/v1/update" + updateCheckTimeout = 10 * time.Second +) + +type UpdateChecker struct { + CurrentVersion string + Platform string + Architecture string + Channel string + Side string + Client *http.Client +} + +func NewServerUpdateChecker(currentVersion, platform, architecture, channel string) *UpdateChecker { + return &UpdateChecker{ + CurrentVersion: currentVersion, + Platform: platform, + Architecture: architecture, + Channel: channel, + Side: "server", + Client: &http.Client{ + Timeout: updateCheckTimeout, + }, + } +} + +// NewClientUpdateChecker ensures that update checks are routed through a HyClient, +// not being sent directly. This safeguard is CRITICAL, especially in scenarios where +// users use Hysteria to bypass censorship. Making direct HTTPS requests to the API +// endpoint could be easily spotted by censors (through SNI, for example), and could +// serve as a signal to identify and penalize Hysteria users. +func NewClientUpdateChecker(currentVersion, platform, architecture, channel string, hyClient client.Client) *UpdateChecker { + return &UpdateChecker{ + CurrentVersion: currentVersion, + Platform: platform, + Architecture: architecture, + Channel: channel, + Side: "client", + Client: &http.Client{ + Timeout: updateCheckTimeout, + Transport: &http.Transport{ + DialContext: func(_ context.Context, network, addr string) (net.Conn, error) { + // Unfortunately HyClient doesn't support context for now + return hyClient.TCP(addr) + }, + }, + }, + } +} + +type UpdateResponse struct { + HasUpdate bool `json:"update"` + LatestVersion string `json:"lver"` + URL string `json:"url"` + Urgent bool `json:"urgent"` +} + +func (uc *UpdateChecker) Check() (*UpdateResponse, error) { + url := fmt.Sprintf("%s?cver=%s&plat=%s&arch=%s&chan=%s&side=%s", + updateCheckEndpoint, + uc.CurrentVersion, + uc.Platform, + uc.Architecture, + uc.Channel, + uc.Side, + ) + resp, err := uc.Client.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + var uResp UpdateResponse + decoder := json.NewDecoder(resp.Body) + if err := decoder.Decode(&uResp); err != nil { + return nil, err + } + return &uResp, nil +} diff --git a/v2rayng/hysteria/app/internal/utils_test/mock.go b/v2rayng/hysteria/app/internal/utils_test/mock.go new file mode 100644 index 0000000000..06057d804f --- /dev/null +++ b/v2rayng/hysteria/app/internal/utils_test/mock.go @@ -0,0 +1,106 @@ +package utils_test + +import ( + "io" + "net" + "time" + + "github.com/apernet/hysteria/core/v2/client" +) + +type MockEchoHyClient struct{} + +func (c *MockEchoHyClient) TCP(addr string) (net.Conn, error) { + return &mockEchoTCPConn{ + BufChan: make(chan []byte, 10), + }, nil +} + +func (c *MockEchoHyClient) UDP() (client.HyUDPConn, error) { + return &mockEchoUDPConn{ + BufChan: make(chan mockEchoUDPPacket, 10), + }, nil +} + +func (c *MockEchoHyClient) Close() error { + return nil +} + +type mockEchoTCPConn struct { + BufChan chan []byte +} + +func (c *mockEchoTCPConn) Read(b []byte) (n int, err error) { + buf := <-c.BufChan + if buf == nil { + // EOF + return 0, io.EOF + } + return copy(b, buf), nil +} + +func (c *mockEchoTCPConn) Write(b []byte) (n int, err error) { + c.BufChan <- b + return len(b), nil +} + +func (c *mockEchoTCPConn) Close() error { + close(c.BufChan) + return nil +} + +func (c *mockEchoTCPConn) LocalAddr() net.Addr { + // Not implemented + return nil +} + +func (c *mockEchoTCPConn) RemoteAddr() net.Addr { + // Not implemented + return nil +} + +func (c *mockEchoTCPConn) SetDeadline(t time.Time) error { + // Not implemented + return nil +} + +func (c *mockEchoTCPConn) SetReadDeadline(t time.Time) error { + // Not implemented + return nil +} + +func (c *mockEchoTCPConn) SetWriteDeadline(t time.Time) error { + // Not implemented + return nil +} + +type mockEchoUDPPacket struct { + Data []byte + Addr string +} + +type mockEchoUDPConn struct { + BufChan chan mockEchoUDPPacket +} + +func (c *mockEchoUDPConn) Receive() ([]byte, string, error) { + p := <-c.BufChan + if p.Data == nil { + // EOF + return nil, "", io.EOF + } + return p.Data, p.Addr, nil +} + +func (c *mockEchoUDPConn) Send(bytes []byte, s string) error { + c.BufChan <- mockEchoUDPPacket{ + Data: bytes, + Addr: s, + } + return nil +} + +func (c *mockEchoUDPConn) Close() error { + close(c.BufChan) + return nil +} diff --git a/v2rayng/hysteria/app/main.go b/v2rayng/hysteria/app/main.go new file mode 100644 index 0000000000..81d8168bbd --- /dev/null +++ b/v2rayng/hysteria/app/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/apernet/hysteria/app/v2/cmd" + +func main() { + cmd.Execute() +} diff --git a/v2rayng/hysteria/app/misc/socks5_test.py b/v2rayng/hysteria/app/misc/socks5_test.py new file mode 100644 index 0000000000..ef9562a8c4 --- /dev/null +++ b/v2rayng/hysteria/app/misc/socks5_test.py @@ -0,0 +1,50 @@ +import socket +import socks +import time + +TARGET = "1.1.1.1" + + +def test_tcp() -> None: + s = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM) + s.set_proxy(socks.SOCKS5, "127.0.0.1", 1080) + + print(f"TCP - Sending HTTP request to {TARGET}") + start = time.time() + s.connect((TARGET, 80)) + s.send(b"GET / HTTP/1.1\r\nHost: " + TARGET.encode() + b"\r\n\r\n") + data = s.recv(1024) + if not data: + print("No data received") + elif not data.startswith(b"HTTP/1.1 "): + print("Invalid response received") + else: + print("TCP test passed") + end = time.time() + s.close() + + print(f"Time: {round((end - start) * 1000, 2)} ms") + + +def test_udp() -> None: + s = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM) + s.set_proxy(socks.SOCKS5, "127.0.0.1", 1080) + + req = b"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x05\x62\x61\x69\x64\x75\x03\x63\x6f\x6d\x00\x00\x01\x00\x01" + print(f"UDP - Sending DNS request to {TARGET}") + start = time.time() + s.sendto(req, (TARGET, 53)) + (rsp, address) = s.recvfrom(4096) + if address[0] == TARGET and address[1] == 53 and rsp[0] == req[0] and rsp[1] == req[1]: + print("UDP test passed") + else: + print("Invalid response received") + end = time.time() + s.close() + + print(f"Time: {round((end - start) * 1000, 2)} ms") + + +if __name__ == "__main__": + test_tcp() + test_udp() diff --git a/v2rayng/hysteria/app/pprof.go b/v2rayng/hysteria/app/pprof.go new file mode 100644 index 0000000000..7a13849730 --- /dev/null +++ b/v2rayng/hysteria/app/pprof.go @@ -0,0 +1,22 @@ +//go:build pprof + +package main + +import ( + "fmt" + "net/http" + _ "net/http/pprof" +) + +const ( + pprofListenAddr = ":6060" +) + +func init() { + fmt.Printf("!!! pprof enabled, listening on %s\n", pprofListenAddr) + go func() { + if err := http.ListenAndServe(pprofListenAddr, nil); err != nil { + panic(err) + } + }() +} diff --git a/v2rayng/hysteria/core/client/.mockery.yaml b/v2rayng/hysteria/core/client/.mockery.yaml new file mode 100644 index 0000000000..299e6f9a3c --- /dev/null +++ b/v2rayng/hysteria/core/client/.mockery.yaml @@ -0,0 +1,9 @@ +with-expecter: true +inpackage: true +dir: . +packages: + github.com/apernet/hysteria/core/v2/client: + interfaces: + udpIO: + config: + mockname: mockUDPIO diff --git a/v2rayng/hysteria/core/client/client.go b/v2rayng/hysteria/core/client/client.go new file mode 100644 index 0000000000..91d59fd3ca --- /dev/null +++ b/v2rayng/hysteria/core/client/client.go @@ -0,0 +1,315 @@ +package client + +import ( + "context" + "crypto/tls" + "net" + "net/http" + "net/url" + "time" + + coreErrs "github.com/apernet/hysteria/core/v2/errors" + "github.com/apernet/hysteria/core/v2/internal/congestion" + "github.com/apernet/hysteria/core/v2/internal/protocol" + "github.com/apernet/hysteria/core/v2/internal/utils" + + "github.com/apernet/quic-go" + "github.com/apernet/quic-go/http3" +) + +const ( + closeErrCodeOK = 0x100 // HTTP3 ErrCodeNoError + closeErrCodeProtocolError = 0x101 // HTTP3 ErrCodeGeneralProtocolError +) + +type Client interface { + TCP(addr string) (net.Conn, error) + UDP() (HyUDPConn, error) + Close() error +} + +type HyUDPConn interface { + Receive() ([]byte, string, error) + Send([]byte, string) error + Close() error +} + +type HandshakeInfo struct { + UDPEnabled bool + Tx uint64 // 0 if using BBR +} + +func NewClient(config *Config) (Client, *HandshakeInfo, error) { + if err := config.verifyAndFill(); err != nil { + return nil, nil, err + } + c := &clientImpl{ + config: config, + } + info, err := c.connect() + if err != nil { + return nil, nil, err + } + return c, info, nil +} + +type clientImpl struct { + config *Config + + pktConn net.PacketConn + conn quic.Connection + + udpSM *udpSessionManager +} + +func (c *clientImpl) connect() (*HandshakeInfo, error) { + pktConn, err := c.config.ConnFactory.New(c.config.ServerAddr) + if err != nil { + return nil, err + } + // Convert config to TLS config & QUIC config + tlsConfig := &tls.Config{ + ServerName: c.config.TLSConfig.ServerName, + InsecureSkipVerify: c.config.TLSConfig.InsecureSkipVerify, + VerifyPeerCertificate: c.config.TLSConfig.VerifyPeerCertificate, + RootCAs: c.config.TLSConfig.RootCAs, + } + quicConfig := &quic.Config{ + InitialStreamReceiveWindow: c.config.QUICConfig.InitialStreamReceiveWindow, + MaxStreamReceiveWindow: c.config.QUICConfig.MaxStreamReceiveWindow, + InitialConnectionReceiveWindow: c.config.QUICConfig.InitialConnectionReceiveWindow, + MaxConnectionReceiveWindow: c.config.QUICConfig.MaxConnectionReceiveWindow, + MaxIdleTimeout: c.config.QUICConfig.MaxIdleTimeout, + KeepAlivePeriod: c.config.QUICConfig.KeepAlivePeriod, + DisablePathMTUDiscovery: c.config.QUICConfig.DisablePathMTUDiscovery, + EnableDatagrams: true, + } + // Prepare RoundTripper + var conn quic.EarlyConnection + rt := &http3.RoundTripper{ + TLSClientConfig: tlsConfig, + QUICConfig: quicConfig, + Dial: func(ctx context.Context, _ string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { + qc, err := quic.DialEarly(ctx, pktConn, c.config.ServerAddr, tlsCfg, cfg) + if err != nil { + return nil, err + } + conn = qc + return qc, nil + }, + } + // Send auth HTTP request + req := &http.Request{ + Method: http.MethodPost, + URL: &url.URL{ + Scheme: "https", + Host: protocol.URLHost, + Path: protocol.URLPath, + }, + Header: make(http.Header), + } + protocol.AuthRequestToHeader(req.Header, protocol.AuthRequest{ + Auth: c.config.Auth, + Rx: c.config.BandwidthConfig.MaxRx, + }) + resp, err := rt.RoundTrip(req) + if err != nil { + if conn != nil { + _ = conn.CloseWithError(closeErrCodeProtocolError, "") + } + _ = pktConn.Close() + return nil, coreErrs.ConnectError{Err: err} + } + if resp.StatusCode != protocol.StatusAuthOK { + _ = conn.CloseWithError(closeErrCodeProtocolError, "") + _ = pktConn.Close() + return nil, coreErrs.AuthError{StatusCode: resp.StatusCode} + } + // Auth OK + authResp := protocol.AuthResponseFromHeader(resp.Header) + var actualTx uint64 + if authResp.RxAuto { + // Server asks client to use bandwidth detection, + // ignore local bandwidth config and use BBR + congestion.UseBBR(conn) + } else { + // actualTx = min(serverRx, clientTx) + actualTx = authResp.Rx + if actualTx == 0 || actualTx > c.config.BandwidthConfig.MaxTx { + // Server doesn't have a limit, or our clientTx is smaller than serverRx + actualTx = c.config.BandwidthConfig.MaxTx + } + if actualTx > 0 { + congestion.UseBrutal(conn, actualTx) + } else { + // We don't know our own bandwidth either, use BBR + congestion.UseBBR(conn) + } + } + _ = resp.Body.Close() + + c.pktConn = pktConn + c.conn = conn + if authResp.UDPEnabled { + c.udpSM = newUDPSessionManager(&udpIOImpl{Conn: conn}) + } + return &HandshakeInfo{ + UDPEnabled: authResp.UDPEnabled, + Tx: actualTx, + }, nil +} + +// openStream wraps the stream with QStream, which handles Close() properly +func (c *clientImpl) openStream() (quic.Stream, error) { + stream, err := c.conn.OpenStream() + if err != nil { + return nil, err + } + return &utils.QStream{Stream: stream}, nil +} + +func (c *clientImpl) TCP(addr string) (net.Conn, error) { + stream, err := c.openStream() + if err != nil { + return nil, wrapIfConnectionClosed(err) + } + // Send request + err = protocol.WriteTCPRequest(stream, addr) + if err != nil { + _ = stream.Close() + return nil, wrapIfConnectionClosed(err) + } + if c.config.FastOpen { + // Don't wait for the response when fast open is enabled. + // Return the connection immediately, defer the response handling + // to the first Read() call. + return &tcpConn{ + Orig: stream, + PseudoLocalAddr: c.conn.LocalAddr(), + PseudoRemoteAddr: c.conn.RemoteAddr(), + Established: false, + }, nil + } + // Read response + ok, msg, err := protocol.ReadTCPResponse(stream) + if err != nil { + _ = stream.Close() + return nil, wrapIfConnectionClosed(err) + } + if !ok { + _ = stream.Close() + return nil, coreErrs.DialError{Message: msg} + } + return &tcpConn{ + Orig: stream, + PseudoLocalAddr: c.conn.LocalAddr(), + PseudoRemoteAddr: c.conn.RemoteAddr(), + Established: true, + }, nil +} + +func (c *clientImpl) UDP() (HyUDPConn, error) { + if c.udpSM == nil { + return nil, coreErrs.DialError{Message: "UDP not enabled"} + } + return c.udpSM.NewUDP() +} + +func (c *clientImpl) Close() error { + _ = c.conn.CloseWithError(closeErrCodeOK, "") + _ = c.pktConn.Close() + return nil +} + +// wrapIfConnectionClosed checks if the error returned by quic-go +// indicates that the QUIC connection has been permanently closed, +// and if so, wraps the error with coreErrs.ClosedError. +// PITFALL: sometimes quic-go has "internal errors" that are not net.Error, +// but we still need to treat them as ClosedError. +func wrapIfConnectionClosed(err error) error { + netErr, ok := err.(net.Error) + if !ok || !netErr.Temporary() { + return coreErrs.ClosedError{Err: err} + } else { + return err + } +} + +type tcpConn struct { + Orig quic.Stream + PseudoLocalAddr net.Addr + PseudoRemoteAddr net.Addr + Established bool +} + +func (c *tcpConn) Read(b []byte) (n int, err error) { + if !c.Established { + // Read response + ok, msg, err := protocol.ReadTCPResponse(c.Orig) + if err != nil { + return 0, err + } + if !ok { + return 0, coreErrs.DialError{Message: msg} + } + c.Established = true + } + return c.Orig.Read(b) +} + +func (c *tcpConn) Write(b []byte) (n int, err error) { + return c.Orig.Write(b) +} + +func (c *tcpConn) Close() error { + return c.Orig.Close() +} + +func (c *tcpConn) LocalAddr() net.Addr { + return c.PseudoLocalAddr +} + +func (c *tcpConn) RemoteAddr() net.Addr { + return c.PseudoRemoteAddr +} + +func (c *tcpConn) SetDeadline(t time.Time) error { + return c.Orig.SetDeadline(t) +} + +func (c *tcpConn) SetReadDeadline(t time.Time) error { + return c.Orig.SetReadDeadline(t) +} + +func (c *tcpConn) SetWriteDeadline(t time.Time) error { + return c.Orig.SetWriteDeadline(t) +} + +type udpIOImpl struct { + Conn quic.Connection +} + +func (io *udpIOImpl) ReceiveMessage() (*protocol.UDPMessage, error) { + for { + msg, err := io.Conn.ReceiveDatagram(context.Background()) + if err != nil { + // Connection error, this will stop the session manager + return nil, err + } + udpMsg, err := protocol.ParseUDPMessage(msg) + if err != nil { + // Invalid message, this is fine - just wait for the next + continue + } + return udpMsg, nil + } +} + +func (io *udpIOImpl) SendMessage(buf []byte, msg *protocol.UDPMessage) error { + msgN := msg.Serialize(buf) + if msgN < 0 { + // Message larger than buffer, silent drop + return nil + } + return io.Conn.SendDatagram(buf[:msgN]) +} diff --git a/v2rayng/hysteria/core/client/config.go b/v2rayng/hysteria/core/client/config.go new file mode 100644 index 0000000000..7270c30b42 --- /dev/null +++ b/v2rayng/hysteria/core/client/config.go @@ -0,0 +1,112 @@ +package client + +import ( + "crypto/x509" + "net" + "time" + + "github.com/apernet/hysteria/core/v2/errors" + "github.com/apernet/hysteria/core/v2/internal/pmtud" +) + +const ( + defaultStreamReceiveWindow = 8388608 // 8MB + defaultConnReceiveWindow = defaultStreamReceiveWindow * 5 / 2 // 20MB + defaultMaxIdleTimeout = 30 * time.Second + defaultKeepAlivePeriod = 10 * time.Second +) + +type Config struct { + ConnFactory ConnFactory + ServerAddr net.Addr + Auth string + TLSConfig TLSConfig + QUICConfig QUICConfig + BandwidthConfig BandwidthConfig + FastOpen bool + + filled bool // whether the fields have been verified and filled +} + +// verifyAndFill fills the fields that are not set by the user with default values when possible, +// and returns an error if the user has not set a required field or has set an invalid value. +func (c *Config) verifyAndFill() error { + if c.filled { + return nil + } + if c.ConnFactory == nil { + c.ConnFactory = &udpConnFactory{} + } + if c.ServerAddr == nil { + return errors.ConfigError{Field: "ServerAddr", Reason: "must be set"} + } + if c.QUICConfig.InitialStreamReceiveWindow == 0 { + c.QUICConfig.InitialStreamReceiveWindow = defaultStreamReceiveWindow + } else if c.QUICConfig.InitialStreamReceiveWindow < 16384 { + return errors.ConfigError{Field: "QUICConfig.InitialStreamReceiveWindow", Reason: "must be at least 16384"} + } + if c.QUICConfig.MaxStreamReceiveWindow == 0 { + c.QUICConfig.MaxStreamReceiveWindow = defaultStreamReceiveWindow + } else if c.QUICConfig.MaxStreamReceiveWindow < 16384 { + return errors.ConfigError{Field: "QUICConfig.MaxStreamReceiveWindow", Reason: "must be at least 16384"} + } + if c.QUICConfig.InitialConnectionReceiveWindow == 0 { + c.QUICConfig.InitialConnectionReceiveWindow = defaultConnReceiveWindow + } else if c.QUICConfig.InitialConnectionReceiveWindow < 16384 { + return errors.ConfigError{Field: "QUICConfig.InitialConnectionReceiveWindow", Reason: "must be at least 16384"} + } + if c.QUICConfig.MaxConnectionReceiveWindow == 0 { + c.QUICConfig.MaxConnectionReceiveWindow = defaultConnReceiveWindow + } else if c.QUICConfig.MaxConnectionReceiveWindow < 16384 { + return errors.ConfigError{Field: "QUICConfig.MaxConnectionReceiveWindow", Reason: "must be at least 16384"} + } + if c.QUICConfig.MaxIdleTimeout == 0 { + c.QUICConfig.MaxIdleTimeout = defaultMaxIdleTimeout + } else if c.QUICConfig.MaxIdleTimeout < 4*time.Second || c.QUICConfig.MaxIdleTimeout > 120*time.Second { + return errors.ConfigError{Field: "QUICConfig.MaxIdleTimeout", Reason: "must be between 4s and 120s"} + } + if c.QUICConfig.KeepAlivePeriod == 0 { + c.QUICConfig.KeepAlivePeriod = defaultKeepAlivePeriod + } else if c.QUICConfig.KeepAlivePeriod < 2*time.Second || c.QUICConfig.KeepAlivePeriod > 60*time.Second { + return errors.ConfigError{Field: "QUICConfig.KeepAlivePeriod", Reason: "must be between 2s and 60s"} + } + c.QUICConfig.DisablePathMTUDiscovery = c.QUICConfig.DisablePathMTUDiscovery || pmtud.DisablePathMTUDiscovery + + c.filled = true + return nil +} + +type ConnFactory interface { + New(net.Addr) (net.PacketConn, error) +} + +type udpConnFactory struct{} + +func (f *udpConnFactory) New(addr net.Addr) (net.PacketConn, error) { + return net.ListenUDP("udp", nil) +} + +// TLSConfig contains the TLS configuration fields that we want to expose to the user. +type TLSConfig struct { + ServerName string + InsecureSkipVerify bool + VerifyPeerCertificate func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error + RootCAs *x509.CertPool +} + +// QUICConfig contains the QUIC configuration fields that we want to expose to the user. +type QUICConfig struct { + InitialStreamReceiveWindow uint64 + MaxStreamReceiveWindow uint64 + InitialConnectionReceiveWindow uint64 + MaxConnectionReceiveWindow uint64 + MaxIdleTimeout time.Duration + KeepAlivePeriod time.Duration + DisablePathMTUDiscovery bool // The server may still override this to true on unsupported platforms. +} + +// BandwidthConfig describes the maximum bandwidth that the server can use, in bytes per second. +type BandwidthConfig struct { + MaxTx uint64 + MaxRx uint64 +} diff --git a/v2rayng/hysteria/core/client/mock_udpIO.go b/v2rayng/hysteria/core/client/mock_udpIO.go new file mode 100644 index 0000000000..aa1444ed10 --- /dev/null +++ b/v2rayng/hysteria/core/client/mock_udpIO.go @@ -0,0 +1,139 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package client + +import ( + protocol "github.com/apernet/hysteria/core/v2/internal/protocol" + mock "github.com/stretchr/testify/mock" +) + +// mockUDPIO is an autogenerated mock type for the udpIO type +type mockUDPIO struct { + mock.Mock +} + +type mockUDPIO_Expecter struct { + mock *mock.Mock +} + +func (_m *mockUDPIO) EXPECT() *mockUDPIO_Expecter { + return &mockUDPIO_Expecter{mock: &_m.Mock} +} + +// ReceiveMessage provides a mock function with given fields: +func (_m *mockUDPIO) ReceiveMessage() (*protocol.UDPMessage, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ReceiveMessage") + } + + var r0 *protocol.UDPMessage + var r1 error + if rf, ok := ret.Get(0).(func() (*protocol.UDPMessage, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *protocol.UDPMessage); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*protocol.UDPMessage) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockUDPIO_ReceiveMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReceiveMessage' +type mockUDPIO_ReceiveMessage_Call struct { + *mock.Call +} + +// ReceiveMessage is a helper method to define mock.On call +func (_e *mockUDPIO_Expecter) ReceiveMessage() *mockUDPIO_ReceiveMessage_Call { + return &mockUDPIO_ReceiveMessage_Call{Call: _e.mock.On("ReceiveMessage")} +} + +func (_c *mockUDPIO_ReceiveMessage_Call) Run(run func()) *mockUDPIO_ReceiveMessage_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *mockUDPIO_ReceiveMessage_Call) Return(_a0 *protocol.UDPMessage, _a1 error) *mockUDPIO_ReceiveMessage_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockUDPIO_ReceiveMessage_Call) RunAndReturn(run func() (*protocol.UDPMessage, error)) *mockUDPIO_ReceiveMessage_Call { + _c.Call.Return(run) + return _c +} + +// SendMessage provides a mock function with given fields: _a0, _a1 +func (_m *mockUDPIO) SendMessage(_a0 []byte, _a1 *protocol.UDPMessage) error { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for SendMessage") + } + + var r0 error + if rf, ok := ret.Get(0).(func([]byte, *protocol.UDPMessage) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockUDPIO_SendMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMessage' +type mockUDPIO_SendMessage_Call struct { + *mock.Call +} + +// SendMessage is a helper method to define mock.On call +// - _a0 []byte +// - _a1 *protocol.UDPMessage +func (_e *mockUDPIO_Expecter) SendMessage(_a0 interface{}, _a1 interface{}) *mockUDPIO_SendMessage_Call { + return &mockUDPIO_SendMessage_Call{Call: _e.mock.On("SendMessage", _a0, _a1)} +} + +func (_c *mockUDPIO_SendMessage_Call) Run(run func(_a0 []byte, _a1 *protocol.UDPMessage)) *mockUDPIO_SendMessage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte), args[1].(*protocol.UDPMessage)) + }) + return _c +} + +func (_c *mockUDPIO_SendMessage_Call) Return(_a0 error) *mockUDPIO_SendMessage_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockUDPIO_SendMessage_Call) RunAndReturn(run func([]byte, *protocol.UDPMessage) error) *mockUDPIO_SendMessage_Call { + _c.Call.Return(run) + return _c +} + +// newMockUDPIO creates a new instance of mockUDPIO. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockUDPIO(t interface { + mock.TestingT + Cleanup(func()) +}) *mockUDPIO { + mock := &mockUDPIO{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/v2rayng/hysteria/core/client/reconnect.go b/v2rayng/hysteria/core/client/reconnect.go new file mode 100644 index 0000000000..91c7eb453c --- /dev/null +++ b/v2rayng/hysteria/core/client/reconnect.go @@ -0,0 +1,120 @@ +package client + +import ( + "net" + "sync" + + coreErrs "github.com/apernet/hysteria/core/v2/errors" +) + +// reconnectableClientImpl is a wrapper of Client, which can reconnect when the connection is closed, +// except when the caller explicitly calls Close() to permanently close this client. +type reconnectableClientImpl struct { + configFunc func() (*Config, error) // called before connecting + connectedFunc func(Client, *HandshakeInfo, int) // called when successfully connected + client Client + count int + m sync.Mutex + closed bool // permanent close +} + +// NewReconnectableClient creates a reconnectable client. +// If lazy is true, the client will not connect until the first call to TCP() or UDP(). +// We use a function for config mainly to delay config evaluation +// (which involves DNS resolution) until the actual connection attempt. +func NewReconnectableClient(configFunc func() (*Config, error), connectedFunc func(Client, *HandshakeInfo, int), lazy bool) (Client, error) { + rc := &reconnectableClientImpl{ + configFunc: configFunc, + connectedFunc: connectedFunc, + } + if !lazy { + if err := rc.reconnect(); err != nil { + return nil, err + } + } + return rc, nil +} + +func (rc *reconnectableClientImpl) reconnect() error { + if rc.client != nil { + _ = rc.client.Close() + } + var info *HandshakeInfo + config, err := rc.configFunc() + if err != nil { + return err + } + rc.client, info, err = NewClient(config) + if err != nil { + return err + } else { + rc.count++ + if rc.connectedFunc != nil { + rc.connectedFunc(rc, info, rc.count) + } + return nil + } +} + +// clientDo calls f with the current client. +// If the client is nil, it will first reconnect. +// It will also detect if the client is closed, and if so, +// set it to nil for reconnect next time. +func (rc *reconnectableClientImpl) clientDo(f func(Client) (interface{}, error)) (interface{}, error) { + rc.m.Lock() + if rc.closed { + rc.m.Unlock() + return nil, coreErrs.ClosedError{} + } + if rc.client == nil { + // No active connection, connect first + if err := rc.reconnect(); err != nil { + rc.m.Unlock() + return nil, err + } + } + client := rc.client + rc.m.Unlock() + + ret, err := f(client) + if _, ok := err.(coreErrs.ClosedError); ok { + // Connection closed, set client to nil for reconnect next time + rc.m.Lock() + if rc.client == client { + // This check is in case the client is already changed by another goroutine + rc.client = nil + } + rc.m.Unlock() + } + return ret, err +} + +func (rc *reconnectableClientImpl) TCP(addr string) (net.Conn, error) { + if c, err := rc.clientDo(func(client Client) (interface{}, error) { + return client.TCP(addr) + }); err != nil { + return nil, err + } else { + return c.(net.Conn), nil + } +} + +func (rc *reconnectableClientImpl) UDP() (HyUDPConn, error) { + if c, err := rc.clientDo(func(client Client) (interface{}, error) { + return client.UDP() + }); err != nil { + return nil, err + } else { + return c.(HyUDPConn), nil + } +} + +func (rc *reconnectableClientImpl) Close() error { + rc.m.Lock() + defer rc.m.Unlock() + rc.closed = true + if rc.client != nil { + return rc.client.Close() + } + return nil +} diff --git a/v2rayng/hysteria/core/client/udp.go b/v2rayng/hysteria/core/client/udp.go new file mode 100644 index 0000000000..ca98095580 --- /dev/null +++ b/v2rayng/hysteria/core/client/udp.go @@ -0,0 +1,185 @@ +package client + +import ( + "errors" + "io" + "math/rand" + "sync" + + "github.com/apernet/quic-go" + + coreErrs "github.com/apernet/hysteria/core/v2/errors" + "github.com/apernet/hysteria/core/v2/internal/frag" + "github.com/apernet/hysteria/core/v2/internal/protocol" +) + +const ( + udpMessageChanSize = 1024 +) + +type udpIO interface { + ReceiveMessage() (*protocol.UDPMessage, error) + SendMessage([]byte, *protocol.UDPMessage) error +} + +type udpConn struct { + ID uint32 + D *frag.Defragger + ReceiveCh chan *protocol.UDPMessage + SendBuf []byte + SendFunc func([]byte, *protocol.UDPMessage) error + CloseFunc func() + Closed bool +} + +func (u *udpConn) Receive() ([]byte, string, error) { + for { + msg := <-u.ReceiveCh + if msg == nil { + // Closed + return nil, "", io.EOF + } + dfMsg := u.D.Feed(msg) + if dfMsg == nil { + // Incomplete message, wait for more + continue + } + return dfMsg.Data, dfMsg.Addr, nil + } +} + +// Send is not thread-safe, as it uses a shared SendBuf. +func (u *udpConn) Send(data []byte, addr string) error { + // Try no frag first + msg := &protocol.UDPMessage{ + SessionID: u.ID, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: addr, + Data: data, + } + err := u.SendFunc(u.SendBuf, msg) + var errTooLarge *quic.DatagramTooLargeError + if errors.As(err, &errTooLarge) { + // Message too large, try fragmentation + msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1 + fMsgs := frag.FragUDPMessage(msg, int(errTooLarge.MaxDataLen)) + for _, fMsg := range fMsgs { + err := u.SendFunc(u.SendBuf, &fMsg) + if err != nil { + return err + } + } + return nil + } else { + return err + } +} + +func (u *udpConn) Close() error { + u.CloseFunc() + return nil +} + +type udpSessionManager struct { + io udpIO + + mutex sync.RWMutex + m map[uint32]*udpConn + nextID uint32 + + closed bool +} + +func newUDPSessionManager(io udpIO) *udpSessionManager { + m := &udpSessionManager{ + io: io, + m: make(map[uint32]*udpConn), + nextID: 1, + } + go m.run() + return m +} + +func (m *udpSessionManager) run() error { + defer m.closeCleanup() + for { + msg, err := m.io.ReceiveMessage() + if err != nil { + return err + } + m.feed(msg) + } +} + +func (m *udpSessionManager) closeCleanup() { + m.mutex.Lock() + defer m.mutex.Unlock() + + for _, conn := range m.m { + m.close(conn) + } + m.closed = true +} + +func (m *udpSessionManager) feed(msg *protocol.UDPMessage) { + m.mutex.RLock() + defer m.mutex.RUnlock() + + conn, ok := m.m[msg.SessionID] + if !ok { + // Ignore message from unknown session + return + } + + select { + case conn.ReceiveCh <- msg: + // OK + default: + // Channel full, drop the message + } +} + +// NewUDP creates a new UDP session. +func (m *udpSessionManager) NewUDP() (HyUDPConn, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + + if m.closed { + return nil, coreErrs.ClosedError{} + } + + id := m.nextID + m.nextID++ + + conn := &udpConn{ + ID: id, + D: &frag.Defragger{}, + ReceiveCh: make(chan *protocol.UDPMessage, udpMessageChanSize), + SendBuf: make([]byte, protocol.MaxUDPSize), + SendFunc: m.io.SendMessage, + } + conn.CloseFunc = func() { + m.mutex.Lock() + defer m.mutex.Unlock() + m.close(conn) + } + m.m[id] = conn + + return conn, nil +} + +func (m *udpSessionManager) close(conn *udpConn) { + if !conn.Closed { + conn.Closed = true + close(conn.ReceiveCh) + delete(m.m, conn.ID) + } +} + +func (m *udpSessionManager) Count() int { + m.mutex.RLock() + defer m.mutex.RUnlock() + return len(m.m) +} diff --git a/v2rayng/hysteria/core/client/udp_test.go b/v2rayng/hysteria/core/client/udp_test.go new file mode 100644 index 0000000000..af1a6d0b78 --- /dev/null +++ b/v2rayng/hysteria/core/client/udp_test.go @@ -0,0 +1,122 @@ +package client + +import ( + "errors" + io2 "io" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "go.uber.org/goleak" + + coreErrs "github.com/apernet/hysteria/core/v2/errors" + "github.com/apernet/hysteria/core/v2/internal/protocol" +) + +func TestUDPSessionManager(t *testing.T) { + io := newMockUDPIO(t) + receiveCh := make(chan *protocol.UDPMessage, 4) + io.EXPECT().ReceiveMessage().RunAndReturn(func() (*protocol.UDPMessage, error) { + m := <-receiveCh + if m == nil { + return nil, errors.New("closed") + } + return m, nil + }) + sm := newUDPSessionManager(io) + + // Test UDP session IO + udpConn1, err := sm.NewUDP() + assert.NoError(t, err) + udpConn2, err := sm.NewUDP() + assert.NoError(t, err) + + msg1 := &protocol.UDPMessage{ + SessionID: 1, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: "random.site.com:9000", + Data: []byte("hello friend"), + } + io.EXPECT().SendMessage(mock.Anything, msg1).Return(nil).Once() + err = udpConn1.Send(msg1.Data, msg1.Addr) + assert.NoError(t, err) + + msg2 := &protocol.UDPMessage{ + SessionID: 2, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: "another.site.org:8000", + Data: []byte("mr robot"), + } + io.EXPECT().SendMessage(mock.Anything, msg2).Return(nil).Once() + err = udpConn2.Send(msg2.Data, msg2.Addr) + assert.NoError(t, err) + + respMsg1 := &protocol.UDPMessage{ + SessionID: 1, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: msg1.Addr, + Data: []byte("goodbye captain price"), + } + receiveCh <- respMsg1 + data, addr, err := udpConn1.Receive() + assert.NoError(t, err) + assert.Equal(t, data, respMsg1.Data) + assert.Equal(t, addr, respMsg1.Addr) + + respMsg2 := &protocol.UDPMessage{ + SessionID: 2, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: msg2.Addr, + Data: []byte("white rose"), + } + receiveCh <- respMsg2 + data, addr, err = udpConn2.Receive() + assert.NoError(t, err) + assert.Equal(t, data, respMsg2.Data) + assert.Equal(t, addr, respMsg2.Addr) + + respMsg3 := &protocol.UDPMessage{ + SessionID: 55, // Bogus session ID that doesn't exist + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: "burgerking.com:27017", + Data: []byte("impossible whopper"), + } + receiveCh <- respMsg3 + // No test for this, just make sure it doesn't panic + + // Test close UDP connection unblocks Receive() + errChan := make(chan error, 1) + go func() { + _, _, err := udpConn1.Receive() + errChan <- err + }() + assert.NoError(t, udpConn1.Close()) + assert.Equal(t, <-errChan, io2.EOF) + + // Test close IO unblocks Receive() and blocks new UDP creation + errChan = make(chan error, 1) + go func() { + _, _, err := udpConn2.Receive() + errChan <- err + }() + close(receiveCh) + assert.Equal(t, <-errChan, io2.EOF) + _, err = sm.NewUDP() + assert.Equal(t, err, coreErrs.ClosedError{}) + + // Leak checks + time.Sleep(1 * time.Second) + assert.Zero(t, sm.Count(), "session count should be 0") + goleak.VerifyNone(t) +} diff --git a/v2rayng/hysteria/core/errors/errors.go b/v2rayng/hysteria/core/errors/errors.go new file mode 100644 index 0000000000..cb691184bd --- /dev/null +++ b/v2rayng/hysteria/core/errors/errors.go @@ -0,0 +1,75 @@ +package errors + +import ( + "fmt" + "strconv" +) + +// ConfigError is returned when a configuration field is invalid. +type ConfigError struct { + Field string + Reason string +} + +func (c ConfigError) Error() string { + return fmt.Sprintf("invalid config: %s: %s", c.Field, c.Reason) +} + +// ConnectError is returned when the client fails to connect to the server. +type ConnectError struct { + Err error +} + +func (c ConnectError) Error() string { + return "connect error: " + c.Err.Error() +} + +func (c ConnectError) Unwrap() error { + return c.Err +} + +// AuthError is returned when the client fails to authenticate with the server. +type AuthError struct { + StatusCode int +} + +func (a AuthError) Error() string { + return "authentication error, HTTP status code: " + strconv.Itoa(a.StatusCode) +} + +// DialError is returned when the server rejects the client's dial request. +// This applies to both TCP and UDP. +type DialError struct { + Message string +} + +func (c DialError) Error() string { + return "dial error: " + c.Message +} + +// ClosedError is returned when the client attempts to use a closed connection. +type ClosedError struct { + Err error // Can be nil +} + +func (c ClosedError) Error() string { + if c.Err == nil { + return "connection closed" + } else { + return "connection closed: " + c.Err.Error() + } +} + +func (c ClosedError) Unwrap() error { + return c.Err +} + +// ProtocolError is returned when the server/client runs into an unexpected +// or malformed request/response/message. +type ProtocolError struct { + Message string +} + +func (p ProtocolError) Error() string { + return "protocol error: " + p.Message +} diff --git a/v2rayng/hysteria/core/go.mod b/v2rayng/hysteria/core/go.mod new file mode 100644 index 0000000000..beb0372201 --- /dev/null +++ b/v2rayng/hysteria/core/go.mod @@ -0,0 +1,36 @@ +module github.com/apernet/hysteria/core/v2 + +go 1.22 + +toolchain go1.23.2 + +require ( + github.com/apernet/quic-go v0.48.2-0.20241104191913-cb103fcecfe7 + github.com/stretchr/testify v1.9.0 + go.uber.org/goleak v1.2.1 + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 + golang.org/x/time v0.5.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/onsi/ginkgo/v2 v2.9.5 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + go.uber.org/mock v0.4.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/v2rayng/hysteria/core/go.sum b/v2rayng/hysteria/core/go.sum new file mode 100644 index 0000000000..d65498b2c7 --- /dev/null +++ b/v2rayng/hysteria/core/go.sum @@ -0,0 +1,76 @@ +github.com/apernet/quic-go v0.48.2-0.20241104191913-cb103fcecfe7 h1:zO38yBOvQ1dLHbSuaU5BFZ8zalnSDQslj+i/9AGOk9s= +github.com/apernet/quic-go v0.48.2-0.20241104191913-cb103fcecfe7/go.mod h1:LoSUY2chVqNQCDyi4IZGqPpXLy1FuCkE37PKwtJvNGg= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v2rayng/hysteria/core/internal/congestion/bbr/bandwidth.go b/v2rayng/hysteria/core/internal/congestion/bbr/bandwidth.go new file mode 100644 index 0000000000..52deb24965 --- /dev/null +++ b/v2rayng/hysteria/core/internal/congestion/bbr/bandwidth.go @@ -0,0 +1,27 @@ +package bbr + +import ( + "math" + "time" + + "github.com/apernet/quic-go/congestion" +) + +const ( + infBandwidth = Bandwidth(math.MaxUint64) +) + +// Bandwidth of a connection +type Bandwidth uint64 + +const ( + // BitsPerSecond is 1 bit per second + BitsPerSecond Bandwidth = 1 + // BytesPerSecond is 1 byte per second + BytesPerSecond = 8 * BitsPerSecond +) + +// BandwidthFromDelta calculates the bandwidth from a number of bytes and a time delta +func BandwidthFromDelta(bytes congestion.ByteCount, delta time.Duration) Bandwidth { + return Bandwidth(bytes) * Bandwidth(time.Second) / Bandwidth(delta) * BytesPerSecond +} diff --git a/v2rayng/hysteria/core/internal/congestion/bbr/bandwidth_sampler.go b/v2rayng/hysteria/core/internal/congestion/bbr/bandwidth_sampler.go new file mode 100644 index 0000000000..0e770f1e4a --- /dev/null +++ b/v2rayng/hysteria/core/internal/congestion/bbr/bandwidth_sampler.go @@ -0,0 +1,874 @@ +package bbr + +import ( + "math" + "time" + + "github.com/apernet/quic-go/congestion" +) + +const ( + infRTT = time.Duration(math.MaxInt64) + defaultConnectionStateMapQueueSize = 256 + defaultCandidatesBufferSize = 256 +) + +type roundTripCount uint64 + +// SendTimeState is a subset of ConnectionStateOnSentPacket which is returned +// to the caller when the packet is acked or lost. +type sendTimeState struct { + // Whether other states in this object is valid. + isValid bool + // Whether the sender is app limited at the time the packet was sent. + // App limited bandwidth sample might be artificially low because the sender + // did not have enough data to send in order to saturate the link. + isAppLimited bool + // Total number of sent bytes at the time the packet was sent. + // Includes the packet itself. + totalBytesSent congestion.ByteCount + // Total number of acked bytes at the time the packet was sent. + totalBytesAcked congestion.ByteCount + // Total number of lost bytes at the time the packet was sent. + totalBytesLost congestion.ByteCount + // Total number of inflight bytes at the time the packet was sent. + // Includes the packet itself. + // It should be equal to |total_bytes_sent| minus the sum of + // |total_bytes_acked|, |total_bytes_lost| and total neutered bytes. + bytesInFlight congestion.ByteCount +} + +func newSendTimeState( + isAppLimited bool, + totalBytesSent congestion.ByteCount, + totalBytesAcked congestion.ByteCount, + totalBytesLost congestion.ByteCount, + bytesInFlight congestion.ByteCount, +) *sendTimeState { + return &sendTimeState{ + isValid: true, + isAppLimited: isAppLimited, + totalBytesSent: totalBytesSent, + totalBytesAcked: totalBytesAcked, + totalBytesLost: totalBytesLost, + bytesInFlight: bytesInFlight, + } +} + +type extraAckedEvent struct { + // The excess bytes acknowlwedged in the time delta for this event. + extraAcked congestion.ByteCount + + // The bytes acknowledged and time delta from the event. + bytesAcked congestion.ByteCount + timeDelta time.Duration + // The round trip of the event. + round roundTripCount +} + +func maxExtraAckedEventFunc(a, b extraAckedEvent) int { + if a.extraAcked > b.extraAcked { + return 1 + } else if a.extraAcked < b.extraAcked { + return -1 + } + return 0 +} + +// BandwidthSample +type bandwidthSample struct { + // The bandwidth at that particular sample. Zero if no valid bandwidth sample + // is available. + bandwidth Bandwidth + // The RTT measurement at this particular sample. Zero if no RTT sample is + // available. Does not correct for delayed ack time. + rtt time.Duration + // |send_rate| is computed from the current packet being acked('P') and an + // earlier packet that is acked before P was sent. + sendRate Bandwidth + // States captured when the packet was sent. + stateAtSend sendTimeState +} + +func newBandwidthSample() *bandwidthSample { + return &bandwidthSample{ + sendRate: infBandwidth, + } +} + +// MaxAckHeightTracker is part of the BandwidthSampler. It is called after every +// ack event to keep track the degree of ack aggregation(a.k.a "ack height"). +type maxAckHeightTracker struct { + // Tracks the maximum number of bytes acked faster than the estimated + // bandwidth. + maxAckHeightFilter *WindowedFilter[extraAckedEvent, roundTripCount] + // The time this aggregation started and the number of bytes acked during it. + aggregationEpochStartTime time.Time + aggregationEpochBytes congestion.ByteCount + // The last sent packet number before the current aggregation epoch started. + lastSentPacketNumberBeforeEpoch congestion.PacketNumber + // The number of ack aggregation epochs ever started, including the ongoing + // one. Stats only. + numAckAggregationEpochs uint64 + ackAggregationBandwidthThreshold float64 + startNewAggregationEpochAfterFullRound bool + reduceExtraAckedOnBandwidthIncrease bool +} + +func newMaxAckHeightTracker(windowLength roundTripCount) *maxAckHeightTracker { + return &maxAckHeightTracker{ + maxAckHeightFilter: NewWindowedFilter(windowLength, maxExtraAckedEventFunc), + lastSentPacketNumberBeforeEpoch: invalidPacketNumber, + ackAggregationBandwidthThreshold: 1.0, + } +} + +func (m *maxAckHeightTracker) Get() congestion.ByteCount { + return m.maxAckHeightFilter.GetBest().extraAcked +} + +func (m *maxAckHeightTracker) Update( + bandwidthEstimate Bandwidth, + isNewMaxBandwidth bool, + roundTripCount roundTripCount, + lastSentPacketNumber congestion.PacketNumber, + lastAckedPacketNumber congestion.PacketNumber, + ackTime time.Time, + bytesAcked congestion.ByteCount, +) congestion.ByteCount { + forceNewEpoch := false + + if m.reduceExtraAckedOnBandwidthIncrease && isNewMaxBandwidth { + // Save and clear existing entries. + best := m.maxAckHeightFilter.GetBest() + secondBest := m.maxAckHeightFilter.GetSecondBest() + thirdBest := m.maxAckHeightFilter.GetThirdBest() + m.maxAckHeightFilter.Clear() + + // Reinsert the heights into the filter after recalculating. + expectedBytesAcked := bytesFromBandwidthAndTimeDelta(bandwidthEstimate, best.timeDelta) + if expectedBytesAcked < best.bytesAcked { + best.extraAcked = best.bytesAcked - expectedBytesAcked + m.maxAckHeightFilter.Update(best, best.round) + } + expectedBytesAcked = bytesFromBandwidthAndTimeDelta(bandwidthEstimate, secondBest.timeDelta) + if expectedBytesAcked < secondBest.bytesAcked { + secondBest.extraAcked = secondBest.bytesAcked - expectedBytesAcked + m.maxAckHeightFilter.Update(secondBest, secondBest.round) + } + expectedBytesAcked = bytesFromBandwidthAndTimeDelta(bandwidthEstimate, thirdBest.timeDelta) + if expectedBytesAcked < thirdBest.bytesAcked { + thirdBest.extraAcked = thirdBest.bytesAcked - expectedBytesAcked + m.maxAckHeightFilter.Update(thirdBest, thirdBest.round) + } + } + + // If any packet sent after the start of the epoch has been acked, start a new + // epoch. + if m.startNewAggregationEpochAfterFullRound && + m.lastSentPacketNumberBeforeEpoch != invalidPacketNumber && + lastAckedPacketNumber != invalidPacketNumber && + lastAckedPacketNumber > m.lastSentPacketNumberBeforeEpoch { + forceNewEpoch = true + } + if m.aggregationEpochStartTime.IsZero() || forceNewEpoch { + m.aggregationEpochBytes = bytesAcked + m.aggregationEpochStartTime = ackTime + m.lastSentPacketNumberBeforeEpoch = lastSentPacketNumber + m.numAckAggregationEpochs++ + return 0 + } + + // Compute how many bytes are expected to be delivered, assuming max bandwidth + // is correct. + aggregationDelta := ackTime.Sub(m.aggregationEpochStartTime) + expectedBytesAcked := bytesFromBandwidthAndTimeDelta(bandwidthEstimate, aggregationDelta) + // Reset the current aggregation epoch as soon as the ack arrival rate is less + // than or equal to the max bandwidth. + if m.aggregationEpochBytes <= congestion.ByteCount(m.ackAggregationBandwidthThreshold*float64(expectedBytesAcked)) { + // Reset to start measuring a new aggregation epoch. + m.aggregationEpochBytes = bytesAcked + m.aggregationEpochStartTime = ackTime + m.lastSentPacketNumberBeforeEpoch = lastSentPacketNumber + m.numAckAggregationEpochs++ + return 0 + } + + m.aggregationEpochBytes += bytesAcked + + // Compute how many extra bytes were delivered vs max bandwidth. + extraBytesAcked := m.aggregationEpochBytes - expectedBytesAcked + newEvent := extraAckedEvent{ + extraAcked: expectedBytesAcked, + bytesAcked: m.aggregationEpochBytes, + timeDelta: aggregationDelta, + } + m.maxAckHeightFilter.Update(newEvent, roundTripCount) + return extraBytesAcked +} + +func (m *maxAckHeightTracker) SetFilterWindowLength(length roundTripCount) { + m.maxAckHeightFilter.SetWindowLength(length) +} + +func (m *maxAckHeightTracker) Reset(newHeight congestion.ByteCount, newTime roundTripCount) { + newEvent := extraAckedEvent{ + extraAcked: newHeight, + round: newTime, + } + m.maxAckHeightFilter.Reset(newEvent, newTime) +} + +func (m *maxAckHeightTracker) SetAckAggregationBandwidthThreshold(threshold float64) { + m.ackAggregationBandwidthThreshold = threshold +} + +func (m *maxAckHeightTracker) SetStartNewAggregationEpochAfterFullRound(value bool) { + m.startNewAggregationEpochAfterFullRound = value +} + +func (m *maxAckHeightTracker) SetReduceExtraAckedOnBandwidthIncrease(value bool) { + m.reduceExtraAckedOnBandwidthIncrease = value +} + +func (m *maxAckHeightTracker) AckAggregationBandwidthThreshold() float64 { + return m.ackAggregationBandwidthThreshold +} + +func (m *maxAckHeightTracker) NumAckAggregationEpochs() uint64 { + return m.numAckAggregationEpochs +} + +// AckPoint represents a point on the ack line. +type ackPoint struct { + ackTime time.Time + totalBytesAcked congestion.ByteCount +} + +// RecentAckPoints maintains the most recent 2 ack points at distinct times. +type recentAckPoints struct { + ackPoints [2]ackPoint +} + +func (r *recentAckPoints) Update(ackTime time.Time, totalBytesAcked congestion.ByteCount) { + if ackTime.Before(r.ackPoints[1].ackTime) { + r.ackPoints[1].ackTime = ackTime + } else if ackTime.After(r.ackPoints[1].ackTime) { + r.ackPoints[0] = r.ackPoints[1] + r.ackPoints[1].ackTime = ackTime + } + + r.ackPoints[1].totalBytesAcked = totalBytesAcked +} + +func (r *recentAckPoints) Clear() { + r.ackPoints[0] = ackPoint{} + r.ackPoints[1] = ackPoint{} +} + +func (r *recentAckPoints) MostRecentPoint() *ackPoint { + return &r.ackPoints[1] +} + +func (r *recentAckPoints) LessRecentPoint() *ackPoint { + if r.ackPoints[0].totalBytesAcked != 0 { + return &r.ackPoints[0] + } + + return &r.ackPoints[1] +} + +// ConnectionStateOnSentPacket represents the information about a sent packet +// and the state of the connection at the moment the packet was sent, +// specifically the information about the most recently acknowledged packet at +// that moment. +type connectionStateOnSentPacket struct { + // Time at which the packet is sent. + sentTime time.Time + // Size of the packet. + size congestion.ByteCount + // The value of |totalBytesSentAtLastAckedPacket| at the time the + // packet was sent. + totalBytesSentAtLastAckedPacket congestion.ByteCount + // The value of |lastAckedPacketSentTime| at the time the packet was + // sent. + lastAckedPacketSentTime time.Time + // The value of |lastAckedPacketAckTime| at the time the packet was + // sent. + lastAckedPacketAckTime time.Time + // Send time states that are returned to the congestion controller when the + // packet is acked or lost. + sendTimeState sendTimeState +} + +// Snapshot constructor. Records the current state of the bandwidth +// sampler. +// |bytes_in_flight| is the bytes in flight right after the packet is sent. +func newConnectionStateOnSentPacket( + sentTime time.Time, + size congestion.ByteCount, + bytesInFlight congestion.ByteCount, + sampler *bandwidthSampler, +) *connectionStateOnSentPacket { + return &connectionStateOnSentPacket{ + sentTime: sentTime, + size: size, + totalBytesSentAtLastAckedPacket: sampler.totalBytesSentAtLastAckedPacket, + lastAckedPacketSentTime: sampler.lastAckedPacketSentTime, + lastAckedPacketAckTime: sampler.lastAckedPacketAckTime, + sendTimeState: *newSendTimeState( + sampler.isAppLimited, + sampler.totalBytesSent, + sampler.totalBytesAcked, + sampler.totalBytesLost, + bytesInFlight, + ), + } +} + +// BandwidthSampler keeps track of sent and acknowledged packets and outputs a +// bandwidth sample for every packet acknowledged. The samples are taken for +// individual packets, and are not filtered; the consumer has to filter the +// bandwidth samples itself. In certain cases, the sampler will locally severely +// underestimate the bandwidth, hence a maximum filter with a size of at least +// one RTT is recommended. +// +// This class bases its samples on the slope of two curves: the number of bytes +// sent over time, and the number of bytes acknowledged as received over time. +// It produces a sample of both slopes for every packet that gets acknowledged, +// based on a slope between two points on each of the corresponding curves. Note +// that due to the packet loss, the number of bytes on each curve might get +// further and further away from each other, meaning that it is not feasible to +// compare byte values coming from different curves with each other. +// +// The obvious points for measuring slope sample are the ones corresponding to +// the packet that was just acknowledged. Let us denote them as S_1 (point at +// which the current packet was sent) and A_1 (point at which the current packet +// was acknowledged). However, taking a slope requires two points on each line, +// so estimating bandwidth requires picking a packet in the past with respect to +// which the slope is measured. +// +// For that purpose, BandwidthSampler always keeps track of the most recently +// acknowledged packet, and records it together with every outgoing packet. +// When a packet gets acknowledged (A_1), it has not only information about when +// it itself was sent (S_1), but also the information about the latest +// acknowledged packet right before it was sent (S_0 and A_0). +// +// Based on that data, send and ack rate are estimated as: +// +// send_rate = (bytes(S_1) - bytes(S_0)) / (time(S_1) - time(S_0)) +// ack_rate = (bytes(A_1) - bytes(A_0)) / (time(A_1) - time(A_0)) +// +// Here, the ack rate is intuitively the rate we want to treat as bandwidth. +// However, in certain cases (e.g. ack compression) the ack rate at a point may +// end up higher than the rate at which the data was originally sent, which is +// not indicative of the real bandwidth. Hence, we use the send rate as an upper +// bound, and the sample value is +// +// rate_sample = min(send_rate, ack_rate) +// +// An important edge case handled by the sampler is tracking the app-limited +// samples. There are multiple meaning of "app-limited" used interchangeably, +// hence it is important to understand and to be able to distinguish between +// them. +// +// Meaning 1: connection state. The connection is said to be app-limited when +// there is no outstanding data to send. This means that certain bandwidth +// samples in the future would not be an accurate indication of the link +// capacity, and it is important to inform consumer about that. Whenever +// connection becomes app-limited, the sampler is notified via OnAppLimited() +// method. +// +// Meaning 2: a phase in the bandwidth sampler. As soon as the bandwidth +// sampler becomes notified about the connection being app-limited, it enters +// app-limited phase. In that phase, all *sent* packets are marked as +// app-limited. Note that the connection itself does not have to be +// app-limited during the app-limited phase, and in fact it will not be +// (otherwise how would it send packets?). The boolean flag below indicates +// whether the sampler is in that phase. +// +// Meaning 3: a flag on the sent packet and on the sample. If a sent packet is +// sent during the app-limited phase, the resulting sample related to the +// packet will be marked as app-limited. +// +// With the terminology issue out of the way, let us consider the question of +// what kind of situation it addresses. +// +// Consider a scenario where we first send packets 1 to 20 at a regular +// bandwidth, and then immediately run out of data. After a few seconds, we send +// packets 21 to 60, and only receive ack for 21 between sending packets 40 and +// 41. In this case, when we sample bandwidth for packets 21 to 40, the S_0/A_0 +// we use to compute the slope is going to be packet 20, a few seconds apart +// from the current packet, hence the resulting estimate would be extremely low +// and not indicative of anything. Only at packet 41 the S_0/A_0 will become 21, +// meaning that the bandwidth sample would exclude the quiescence. +// +// Based on the analysis of that scenario, we implement the following rule: once +// OnAppLimited() is called, all sent packets will produce app-limited samples +// up until an ack for a packet that was sent after OnAppLimited() was called. +// Note that while the scenario above is not the only scenario when the +// connection is app-limited, the approach works in other cases too. + +type congestionEventSample struct { + // The maximum bandwidth sample from all acked packets. + // QuicBandwidth::Zero() if no samples are available. + sampleMaxBandwidth Bandwidth + // Whether |sample_max_bandwidth| is from a app-limited sample. + sampleIsAppLimited bool + // The minimum rtt sample from all acked packets. + // QuicTime::Delta::Infinite() if no samples are available. + sampleRtt time.Duration + // For each packet p in acked packets, this is the max value of INFLIGHT(p), + // where INFLIGHT(p) is the number of bytes acked while p is inflight. + sampleMaxInflight congestion.ByteCount + // The send state of the largest packet in acked_packets, unless it is + // empty. If acked_packets is empty, it's the send state of the largest + // packet in lost_packets. + lastPacketSendState sendTimeState + // The number of extra bytes acked from this ack event, compared to what is + // expected from the flow's bandwidth. Larger value means more ack + // aggregation. + extraAcked congestion.ByteCount +} + +func newCongestionEventSample() *congestionEventSample { + return &congestionEventSample{ + sampleRtt: infRTT, + } +} + +type bandwidthSampler struct { + // The total number of congestion controlled bytes sent during the connection. + totalBytesSent congestion.ByteCount + + // The total number of congestion controlled bytes which were acknowledged. + totalBytesAcked congestion.ByteCount + + // The total number of congestion controlled bytes which were lost. + totalBytesLost congestion.ByteCount + + // The total number of congestion controlled bytes which have been neutered. + totalBytesNeutered congestion.ByteCount + + // The value of |total_bytes_sent_| at the time the last acknowledged packet + // was sent. Valid only when |last_acked_packet_sent_time_| is valid. + totalBytesSentAtLastAckedPacket congestion.ByteCount + + // The time at which the last acknowledged packet was sent. Set to + // QuicTime::Zero() if no valid timestamp is available. + lastAckedPacketSentTime time.Time + + // The time at which the most recent packet was acknowledged. + lastAckedPacketAckTime time.Time + + // The most recently sent packet. + lastSentPacket congestion.PacketNumber + + // The most recently acked packet. + lastAckedPacket congestion.PacketNumber + + // Indicates whether the bandwidth sampler is currently in an app-limited + // phase. + isAppLimited bool + + // The packet that will be acknowledged after this one will cause the sampler + // to exit the app-limited phase. + endOfAppLimitedPhase congestion.PacketNumber + + // Record of the connection state at the point where each packet in flight was + // sent, indexed by the packet number. + connectionStateMap *packetNumberIndexedQueue[connectionStateOnSentPacket] + + recentAckPoints recentAckPoints + a0Candidates RingBuffer[ackPoint] + + // Maximum number of tracked packets. + maxTrackedPackets congestion.ByteCount + + maxAckHeightTracker *maxAckHeightTracker + totalBytesAckedAfterLastAckEvent congestion.ByteCount + + // True if connection option 'BSAO' is set. + overestimateAvoidance bool + + // True if connection option 'BBRB' is set. + limitMaxAckHeightTrackerBySendRate bool +} + +func newBandwidthSampler(maxAckHeightTrackerWindowLength roundTripCount) *bandwidthSampler { + b := &bandwidthSampler{ + maxAckHeightTracker: newMaxAckHeightTracker(maxAckHeightTrackerWindowLength), + connectionStateMap: newPacketNumberIndexedQueue[connectionStateOnSentPacket](defaultConnectionStateMapQueueSize), + lastSentPacket: invalidPacketNumber, + lastAckedPacket: invalidPacketNumber, + endOfAppLimitedPhase: invalidPacketNumber, + } + + b.a0Candidates.Init(defaultCandidatesBufferSize) + + return b +} + +func (b *bandwidthSampler) MaxAckHeight() congestion.ByteCount { + return b.maxAckHeightTracker.Get() +} + +func (b *bandwidthSampler) NumAckAggregationEpochs() uint64 { + return b.maxAckHeightTracker.NumAckAggregationEpochs() +} + +func (b *bandwidthSampler) SetMaxAckHeightTrackerWindowLength(length roundTripCount) { + b.maxAckHeightTracker.SetFilterWindowLength(length) +} + +func (b *bandwidthSampler) ResetMaxAckHeightTracker(newHeight congestion.ByteCount, newTime roundTripCount) { + b.maxAckHeightTracker.Reset(newHeight, newTime) +} + +func (b *bandwidthSampler) SetStartNewAggregationEpochAfterFullRound(value bool) { + b.maxAckHeightTracker.SetStartNewAggregationEpochAfterFullRound(value) +} + +func (b *bandwidthSampler) SetLimitMaxAckHeightTrackerBySendRate(value bool) { + b.limitMaxAckHeightTrackerBySendRate = value +} + +func (b *bandwidthSampler) SetReduceExtraAckedOnBandwidthIncrease(value bool) { + b.maxAckHeightTracker.SetReduceExtraAckedOnBandwidthIncrease(value) +} + +func (b *bandwidthSampler) EnableOverestimateAvoidance() { + if b.overestimateAvoidance { + return + } + + b.overestimateAvoidance = true + b.maxAckHeightTracker.SetAckAggregationBandwidthThreshold(2.0) +} + +func (b *bandwidthSampler) IsOverestimateAvoidanceEnabled() bool { + return b.overestimateAvoidance +} + +func (b *bandwidthSampler) OnPacketSent( + sentTime time.Time, + packetNumber congestion.PacketNumber, + bytes congestion.ByteCount, + bytesInFlight congestion.ByteCount, + isRetransmittable bool, +) { + b.lastSentPacket = packetNumber + + if !isRetransmittable { + return + } + + b.totalBytesSent += bytes + + // If there are no packets in flight, the time at which the new transmission + // opens can be treated as the A_0 point for the purpose of bandwidth + // sampling. This underestimates bandwidth to some extent, and produces some + // artificially low samples for most packets in flight, but it provides with + // samples at important points where we would not have them otherwise, most + // importantly at the beginning of the connection. + if bytesInFlight == 0 { + b.lastAckedPacketAckTime = sentTime + if b.overestimateAvoidance { + b.recentAckPoints.Clear() + b.recentAckPoints.Update(sentTime, b.totalBytesAcked) + b.a0Candidates.Clear() + b.a0Candidates.PushBack(*b.recentAckPoints.MostRecentPoint()) + } + b.totalBytesSentAtLastAckedPacket = b.totalBytesSent + + // In this situation ack compression is not a concern, set send rate to + // effectively infinite. + b.lastAckedPacketSentTime = sentTime + } + + b.connectionStateMap.Emplace(packetNumber, newConnectionStateOnSentPacket( + sentTime, + bytes, + bytesInFlight+bytes, + b, + )) +} + +func (b *bandwidthSampler) OnCongestionEvent( + ackTime time.Time, + ackedPackets []congestion.AckedPacketInfo, + lostPackets []congestion.LostPacketInfo, + maxBandwidth Bandwidth, + estBandwidthUpperBound Bandwidth, + roundTripCount roundTripCount, +) congestionEventSample { + eventSample := newCongestionEventSample() + + var lastLostPacketSendState sendTimeState + + for _, p := range lostPackets { + sendState := b.OnPacketLost(p.PacketNumber, p.BytesLost) + if sendState.isValid { + lastLostPacketSendState = sendState + } + } + + if len(ackedPackets) == 0 { + // Only populate send state for a loss-only event. + eventSample.lastPacketSendState = lastLostPacketSendState + return *eventSample + } + + var lastAckedPacketSendState sendTimeState + var maxSendRate Bandwidth + + for _, p := range ackedPackets { + sample := b.onPacketAcknowledged(ackTime, p.PacketNumber) + if !sample.stateAtSend.isValid { + continue + } + + lastAckedPacketSendState = sample.stateAtSend + + if sample.rtt != 0 { + eventSample.sampleRtt = min(eventSample.sampleRtt, sample.rtt) + } + if sample.bandwidth > eventSample.sampleMaxBandwidth { + eventSample.sampleMaxBandwidth = sample.bandwidth + eventSample.sampleIsAppLimited = sample.stateAtSend.isAppLimited + } + if sample.sendRate != infBandwidth { + maxSendRate = max(maxSendRate, sample.sendRate) + } + inflightSample := b.totalBytesAcked - lastAckedPacketSendState.totalBytesAcked + if inflightSample > eventSample.sampleMaxInflight { + eventSample.sampleMaxInflight = inflightSample + } + } + + if !lastLostPacketSendState.isValid { + eventSample.lastPacketSendState = lastAckedPacketSendState + } else if !lastAckedPacketSendState.isValid { + eventSample.lastPacketSendState = lastLostPacketSendState + } else { + // If two packets are inflight and an alarm is armed to lose a packet and it + // wakes up late, then the first of two in flight packets could have been + // acknowledged before the wakeup, which re-evaluates loss detection, and + // could declare the later of the two lost. + if lostPackets[len(lostPackets)-1].PacketNumber > ackedPackets[len(ackedPackets)-1].PacketNumber { + eventSample.lastPacketSendState = lastLostPacketSendState + } else { + eventSample.lastPacketSendState = lastAckedPacketSendState + } + } + + isNewMaxBandwidth := eventSample.sampleMaxBandwidth > maxBandwidth + maxBandwidth = max(maxBandwidth, eventSample.sampleMaxBandwidth) + if b.limitMaxAckHeightTrackerBySendRate { + maxBandwidth = max(maxBandwidth, maxSendRate) + } + + eventSample.extraAcked = b.onAckEventEnd(min(estBandwidthUpperBound, maxBandwidth), isNewMaxBandwidth, roundTripCount) + + return *eventSample +} + +func (b *bandwidthSampler) OnPacketLost(packetNumber congestion.PacketNumber, bytesLost congestion.ByteCount) (s sendTimeState) { + b.totalBytesLost += bytesLost + if sentPacketPointer := b.connectionStateMap.GetEntry(packetNumber); sentPacketPointer != nil { + sentPacketToSendTimeState(sentPacketPointer, &s) + } + return s +} + +func (b *bandwidthSampler) OnPacketNeutered(packetNumber congestion.PacketNumber) { + b.connectionStateMap.Remove(packetNumber, func(sentPacket connectionStateOnSentPacket) { + b.totalBytesNeutered += sentPacket.size + }) +} + +func (b *bandwidthSampler) OnAppLimited() { + b.isAppLimited = true + b.endOfAppLimitedPhase = b.lastSentPacket +} + +func (b *bandwidthSampler) RemoveObsoletePackets(leastUnacked congestion.PacketNumber) { + // A packet can become obsolete when it is removed from QuicUnackedPacketMap's + // view of inflight before it is acked or marked as lost. For example, when + // QuicSentPacketManager::RetransmitCryptoPackets retransmits a crypto packet, + // the packet is removed from QuicUnackedPacketMap's inflight, but is not + // marked as acked or lost in the BandwidthSampler. + b.connectionStateMap.RemoveUpTo(leastUnacked) +} + +func (b *bandwidthSampler) TotalBytesSent() congestion.ByteCount { + return b.totalBytesSent +} + +func (b *bandwidthSampler) TotalBytesLost() congestion.ByteCount { + return b.totalBytesLost +} + +func (b *bandwidthSampler) TotalBytesAcked() congestion.ByteCount { + return b.totalBytesAcked +} + +func (b *bandwidthSampler) TotalBytesNeutered() congestion.ByteCount { + return b.totalBytesNeutered +} + +func (b *bandwidthSampler) IsAppLimited() bool { + return b.isAppLimited +} + +func (b *bandwidthSampler) EndOfAppLimitedPhase() congestion.PacketNumber { + return b.endOfAppLimitedPhase +} + +func (b *bandwidthSampler) max_ack_height() congestion.ByteCount { + return b.maxAckHeightTracker.Get() +} + +func (b *bandwidthSampler) chooseA0Point(totalBytesAcked congestion.ByteCount, a0 *ackPoint) bool { + if b.a0Candidates.Empty() { + return false + } + + if b.a0Candidates.Len() == 1 { + *a0 = *b.a0Candidates.Front() + return true + } + + for i := 1; i < b.a0Candidates.Len(); i++ { + if b.a0Candidates.Offset(i).totalBytesAcked > totalBytesAcked { + *a0 = *b.a0Candidates.Offset(i - 1) + if i > 1 { + for j := 0; j < i-1; j++ { + b.a0Candidates.PopFront() + } + } + return true + } + } + + *a0 = *b.a0Candidates.Back() + for k := 0; k < b.a0Candidates.Len()-1; k++ { + b.a0Candidates.PopFront() + } + return true +} + +func (b *bandwidthSampler) onPacketAcknowledged(ackTime time.Time, packetNumber congestion.PacketNumber) bandwidthSample { + sample := newBandwidthSample() + b.lastAckedPacket = packetNumber + sentPacketPointer := b.connectionStateMap.GetEntry(packetNumber) + if sentPacketPointer == nil { + return *sample + } + + // OnPacketAcknowledgedInner + b.totalBytesAcked += sentPacketPointer.size + b.totalBytesSentAtLastAckedPacket = sentPacketPointer.sendTimeState.totalBytesSent + b.lastAckedPacketSentTime = sentPacketPointer.sentTime + b.lastAckedPacketAckTime = ackTime + if b.overestimateAvoidance { + b.recentAckPoints.Update(ackTime, b.totalBytesAcked) + } + + if b.isAppLimited { + // Exit app-limited phase in two cases: + // (1) end_of_app_limited_phase_ is not initialized, i.e., so far all + // packets are sent while there are buffered packets or pending data. + // (2) The current acked packet is after the sent packet marked as the end + // of the app limit phase. + if b.endOfAppLimitedPhase == invalidPacketNumber || + packetNumber > b.endOfAppLimitedPhase { + b.isAppLimited = false + } + } + + // There might have been no packets acknowledged at the moment when the + // current packet was sent. In that case, there is no bandwidth sample to + // make. + if sentPacketPointer.lastAckedPacketSentTime.IsZero() { + return *sample + } + + // Infinite rate indicates that the sampler is supposed to discard the + // current send rate sample and use only the ack rate. + sendRate := infBandwidth + if sentPacketPointer.sentTime.After(sentPacketPointer.lastAckedPacketSentTime) { + sendRate = BandwidthFromDelta( + sentPacketPointer.sendTimeState.totalBytesSent-sentPacketPointer.totalBytesSentAtLastAckedPacket, + sentPacketPointer.sentTime.Sub(sentPacketPointer.lastAckedPacketSentTime)) + } + + var a0 ackPoint + if b.overestimateAvoidance && b.chooseA0Point(sentPacketPointer.sendTimeState.totalBytesAcked, &a0) { + } else { + a0.ackTime = sentPacketPointer.lastAckedPacketAckTime + a0.totalBytesAcked = sentPacketPointer.sendTimeState.totalBytesAcked + } + + // During the slope calculation, ensure that ack time of the current packet is + // always larger than the time of the previous packet, otherwise division by + // zero or integer underflow can occur. + if ackTime.Sub(a0.ackTime) <= 0 { + return *sample + } + + ackRate := BandwidthFromDelta(b.totalBytesAcked-a0.totalBytesAcked, ackTime.Sub(a0.ackTime)) + + sample.bandwidth = min(sendRate, ackRate) + // Note: this sample does not account for delayed acknowledgement time. This + // means that the RTT measurements here can be artificially high, especially + // on low bandwidth connections. + sample.rtt = ackTime.Sub(sentPacketPointer.sentTime) + sample.sendRate = sendRate + sentPacketToSendTimeState(sentPacketPointer, &sample.stateAtSend) + + return *sample +} + +func (b *bandwidthSampler) onAckEventEnd( + bandwidthEstimate Bandwidth, + isNewMaxBandwidth bool, + roundTripCount roundTripCount, +) congestion.ByteCount { + newlyAckedBytes := b.totalBytesAcked - b.totalBytesAckedAfterLastAckEvent + if newlyAckedBytes == 0 { + return 0 + } + b.totalBytesAckedAfterLastAckEvent = b.totalBytesAcked + extraAcked := b.maxAckHeightTracker.Update( + bandwidthEstimate, + isNewMaxBandwidth, + roundTripCount, + b.lastSentPacket, + b.lastAckedPacket, + b.lastAckedPacketAckTime, + newlyAckedBytes) + // If |extra_acked| is zero, i.e. this ack event marks the start of a new ack + // aggregation epoch, save LessRecentPoint, which is the last ack point of the + // previous epoch, as a A0 candidate. + if b.overestimateAvoidance && extraAcked == 0 { + b.a0Candidates.PushBack(*b.recentAckPoints.LessRecentPoint()) + } + return extraAcked +} + +func sentPacketToSendTimeState(sentPacket *connectionStateOnSentPacket, sendTimeState *sendTimeState) { + *sendTimeState = sentPacket.sendTimeState + sendTimeState.isValid = true +} + +// BytesFromBandwidthAndTimeDelta calculates the bytes +// from a bandwidth(bits per second) and a time delta +func bytesFromBandwidthAndTimeDelta(bandwidth Bandwidth, delta time.Duration) congestion.ByteCount { + return (congestion.ByteCount(bandwidth) * congestion.ByteCount(delta)) / + (congestion.ByteCount(time.Second) * 8) +} + +func timeDeltaFromBytesAndBandwidth(bytes congestion.ByteCount, bandwidth Bandwidth) time.Duration { + return time.Duration(bytes*8) * time.Second / time.Duration(bandwidth) +} diff --git a/v2rayng/hysteria/core/internal/congestion/bbr/bbr_sender.go b/v2rayng/hysteria/core/internal/congestion/bbr/bbr_sender.go new file mode 100644 index 0000000000..8f58e1f977 --- /dev/null +++ b/v2rayng/hysteria/core/internal/congestion/bbr/bbr_sender.go @@ -0,0 +1,984 @@ +package bbr + +import ( + "fmt" + "math/rand" + "net" + "os" + "strconv" + "time" + + "github.com/apernet/quic-go/congestion" + + "github.com/apernet/hysteria/core/v2/internal/congestion/common" +) + +// BbrSender implements BBR congestion control algorithm. BBR aims to estimate +// the current available Bottleneck Bandwidth and RTT (hence the name), and +// regulates the pacing rate and the size of the congestion window based on +// those signals. +// +// BBR relies on pacing in order to function properly. Do not use BBR when +// pacing is disabled. +// + +const ( + minBps = 65536 // 64 kbps + + invalidPacketNumber = -1 + initialCongestionWindowPackets = 32 + + // Constants based on TCP defaults. + // The minimum CWND to ensure delayed acks don't reduce bandwidth measurements. + // Does not inflate the pacing rate. + defaultMinimumCongestionWindow = 4 * congestion.ByteCount(congestion.InitialPacketSizeIPv4) + + // The gain used for the STARTUP, equal to 2/ln(2). + defaultHighGain = 2.885 + // The newly derived gain for STARTUP, equal to 4 * ln(2) + derivedHighGain = 2.773 + // The newly derived CWND gain for STARTUP, 2. + derivedHighCWNDGain = 2.0 + + debugEnv = "HYSTERIA_BBR_DEBUG" +) + +// The cycle of gains used during the PROBE_BW stage. +var pacingGain = [...]float64{1.25, 0.75, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0} + +const ( + // The length of the gain cycle. + gainCycleLength = len(pacingGain) + // The size of the bandwidth filter window, in round-trips. + bandwidthWindowSize = gainCycleLength + 2 + + // The time after which the current min_rtt value expires. + minRttExpiry = 10 * time.Second + // The minimum time the connection can spend in PROBE_RTT mode. + probeRttTime = 200 * time.Millisecond + // If the bandwidth does not increase by the factor of |kStartupGrowthTarget| + // within |kRoundTripsWithoutGrowthBeforeExitingStartup| rounds, the connection + // will exit the STARTUP mode. + startupGrowthTarget = 1.25 + roundTripsWithoutGrowthBeforeExitingStartup = int64(3) + + // Flag. + defaultStartupFullLossCount = 8 + quicBbr2DefaultLossThreshold = 0.02 + maxBbrBurstPackets = 10 +) + +type bbrMode int + +const ( + // Startup phase of the connection. + bbrModeStartup = iota + // After achieving the highest possible bandwidth during the startup, lower + // the pacing rate in order to drain the queue. + bbrModeDrain + // Cruising mode. + bbrModeProbeBw + // Temporarily slow down sending in order to empty the buffer and measure + // the real minimum RTT. + bbrModeProbeRtt +) + +// Indicates how the congestion control limits the amount of bytes in flight. +type bbrRecoveryState int + +const ( + // Do not limit. + bbrRecoveryStateNotInRecovery = iota + // Allow an extra outstanding byte for each byte acknowledged. + bbrRecoveryStateConservation + // Allow two extra outstanding bytes for each byte acknowledged (slow + // start). + bbrRecoveryStateGrowth +) + +type bbrSender struct { + rttStats congestion.RTTStatsProvider + clock Clock + pacer *common.Pacer + + mode bbrMode + + // Bandwidth sampler provides BBR with the bandwidth measurements at + // individual points. + sampler *bandwidthSampler + + // The number of the round trips that have occurred during the connection. + roundTripCount roundTripCount + + // The packet number of the most recently sent packet. + lastSentPacket congestion.PacketNumber + // Acknowledgement of any packet after |current_round_trip_end_| will cause + // the round trip counter to advance. + currentRoundTripEnd congestion.PacketNumber + + // Number of congestion events with some losses, in the current round. + numLossEventsInRound uint64 + + // Number of total bytes lost in the current round. + bytesLostInRound congestion.ByteCount + + // The filter that tracks the maximum bandwidth over the multiple recent + // round-trips. + maxBandwidth *WindowedFilter[Bandwidth, roundTripCount] + + // Minimum RTT estimate. Automatically expires within 10 seconds (and + // triggers PROBE_RTT mode) if no new value is sampled during that period. + minRtt time.Duration + // The time at which the current value of |min_rtt_| was assigned. + minRttTimestamp time.Time + + // The maximum allowed number of bytes in flight. + congestionWindow congestion.ByteCount + + // The initial value of the |congestion_window_|. + initialCongestionWindow congestion.ByteCount + + // The largest value the |congestion_window_| can achieve. + maxCongestionWindow congestion.ByteCount + + // The smallest value the |congestion_window_| can achieve. + minCongestionWindow congestion.ByteCount + + // The pacing gain applied during the STARTUP phase. + highGain float64 + + // The CWND gain applied during the STARTUP phase. + highCwndGain float64 + + // The pacing gain applied during the DRAIN phase. + drainGain float64 + + // The current pacing rate of the connection. + pacingRate Bandwidth + + // The gain currently applied to the pacing rate. + pacingGain float64 + // The gain currently applied to the congestion window. + congestionWindowGain float64 + + // The gain used for the congestion window during PROBE_BW. Latched from + // quic_bbr_cwnd_gain flag. + congestionWindowGainConstant float64 + // The number of RTTs to stay in STARTUP mode. Defaults to 3. + numStartupRtts int64 + + // Number of round-trips in PROBE_BW mode, used for determining the current + // pacing gain cycle. + cycleCurrentOffset int + // The time at which the last pacing gain cycle was started. + lastCycleStart time.Time + + // Indicates whether the connection has reached the full bandwidth mode. + isAtFullBandwidth bool + // Number of rounds during which there was no significant bandwidth increase. + roundsWithoutBandwidthGain int64 + // The bandwidth compared to which the increase is measured. + bandwidthAtLastRound Bandwidth + + // Set to true upon exiting quiescence. + exitingQuiescence bool + + // Time at which PROBE_RTT has to be exited. Setting it to zero indicates + // that the time is yet unknown as the number of packets in flight has not + // reached the required value. + exitProbeRttAt time.Time + // Indicates whether a round-trip has passed since PROBE_RTT became active. + probeRttRoundPassed bool + + // Indicates whether the most recent bandwidth sample was marked as + // app-limited. + lastSampleIsAppLimited bool + // Indicates whether any non app-limited samples have been recorded. + hasNoAppLimitedSample bool + + // Current state of recovery. + recoveryState bbrRecoveryState + // Receiving acknowledgement of a packet after |end_recovery_at_| will cause + // BBR to exit the recovery mode. A value above zero indicates at least one + // loss has been detected, so it must not be set back to zero. + endRecoveryAt congestion.PacketNumber + // A window used to limit the number of bytes in flight during loss recovery. + recoveryWindow congestion.ByteCount + // If true, consider all samples in recovery app-limited. + isAppLimitedRecovery bool // not used + + // When true, pace at 1.5x and disable packet conservation in STARTUP. + slowerStartup bool // not used + // When true, disables packet conservation in STARTUP. + rateBasedStartup bool // not used + + // When true, add the most recent ack aggregation measurement during STARTUP. + enableAckAggregationDuringStartup bool + // When true, expire the windowed ack aggregation values in STARTUP when + // bandwidth increases more than 25%. + expireAckAggregationInStartup bool + + // If true, will not exit low gain mode until bytes_in_flight drops below BDP + // or it's time for high gain mode. + drainToTarget bool + + // If true, slow down pacing rate in STARTUP when overshooting is detected. + detectOvershooting bool + // Bytes lost while detect_overshooting_ is true. + bytesLostWhileDetectingOvershooting congestion.ByteCount + // Slow down pacing rate if + // bytes_lost_while_detecting_overshooting_ * + // bytes_lost_multiplier_while_detecting_overshooting_ > IW. + bytesLostMultiplierWhileDetectingOvershooting uint8 + // When overshooting is detected, do not drop pacing_rate_ below this value / + // min_rtt. + cwndToCalculateMinPacingRate congestion.ByteCount + + // Max congestion window when adjusting network parameters. + maxCongestionWindowWithNetworkParametersAdjusted congestion.ByteCount // not used + + // Params. + maxDatagramSize congestion.ByteCount + // Recorded on packet sent. equivalent |unacked_packets_->bytes_in_flight()| + bytesInFlight congestion.ByteCount + + debug bool +} + +var _ congestion.CongestionControl = &bbrSender{} + +func NewBbrSender( + clock Clock, + initialMaxDatagramSize congestion.ByteCount, +) *bbrSender { + return newBbrSender( + clock, + initialMaxDatagramSize, + initialCongestionWindowPackets*initialMaxDatagramSize, + congestion.MaxCongestionWindowPackets*initialMaxDatagramSize, + ) +} + +func newBbrSender( + clock Clock, + initialMaxDatagramSize, + initialCongestionWindow, + initialMaxCongestionWindow congestion.ByteCount, +) *bbrSender { + debug, _ := strconv.ParseBool(os.Getenv(debugEnv)) + b := &bbrSender{ + clock: clock, + mode: bbrModeStartup, + sampler: newBandwidthSampler(roundTripCount(bandwidthWindowSize)), + lastSentPacket: invalidPacketNumber, + currentRoundTripEnd: invalidPacketNumber, + maxBandwidth: NewWindowedFilter(roundTripCount(bandwidthWindowSize), MaxFilter[Bandwidth]), + congestionWindow: initialCongestionWindow, + initialCongestionWindow: initialCongestionWindow, + maxCongestionWindow: initialMaxCongestionWindow, + minCongestionWindow: defaultMinimumCongestionWindow, + highGain: defaultHighGain, + highCwndGain: defaultHighGain, + drainGain: 1.0 / defaultHighGain, + pacingGain: 1.0, + congestionWindowGain: 1.0, + congestionWindowGainConstant: 2.0, + numStartupRtts: roundTripsWithoutGrowthBeforeExitingStartup, + recoveryState: bbrRecoveryStateNotInRecovery, + endRecoveryAt: invalidPacketNumber, + recoveryWindow: initialMaxCongestionWindow, + bytesLostMultiplierWhileDetectingOvershooting: 2, + cwndToCalculateMinPacingRate: initialCongestionWindow, + maxCongestionWindowWithNetworkParametersAdjusted: initialMaxCongestionWindow, + maxDatagramSize: initialMaxDatagramSize, + debug: debug, + } + b.pacer = common.NewPacer(b.bandwidthForPacer) + + /* + if b.tracer != nil { + b.lastState = logging.CongestionStateStartup + b.tracer.UpdatedCongestionState(logging.CongestionStateStartup) + } + */ + + b.enterStartupMode(b.clock.Now()) + b.setHighCwndGain(derivedHighCWNDGain) + + return b +} + +func (b *bbrSender) SetRTTStatsProvider(provider congestion.RTTStatsProvider) { + b.rttStats = provider +} + +// TimeUntilSend implements the SendAlgorithm interface. +func (b *bbrSender) TimeUntilSend(bytesInFlight congestion.ByteCount) time.Time { + return b.pacer.TimeUntilSend() +} + +// HasPacingBudget implements the SendAlgorithm interface. +func (b *bbrSender) HasPacingBudget(now time.Time) bool { + return b.pacer.Budget(now) >= b.maxDatagramSize +} + +// OnPacketSent implements the SendAlgorithm interface. +func (b *bbrSender) OnPacketSent( + sentTime time.Time, + bytesInFlight congestion.ByteCount, + packetNumber congestion.PacketNumber, + bytes congestion.ByteCount, + isRetransmittable bool, +) { + b.pacer.SentPacket(sentTime, bytes) + + b.lastSentPacket = packetNumber + b.bytesInFlight = bytesInFlight + + if bytesInFlight == 0 { + b.exitingQuiescence = true + } + + b.sampler.OnPacketSent(sentTime, packetNumber, bytes, bytesInFlight, isRetransmittable) +} + +// CanSend implements the SendAlgorithm interface. +func (b *bbrSender) CanSend(bytesInFlight congestion.ByteCount) bool { + return bytesInFlight < b.GetCongestionWindow() +} + +// MaybeExitSlowStart implements the SendAlgorithm interface. +func (b *bbrSender) MaybeExitSlowStart() { + // Do nothing +} + +// OnPacketAcked implements the SendAlgorithm interface. +func (b *bbrSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes, priorInFlight congestion.ByteCount, eventTime time.Time) { + // Do nothing. +} + +// OnPacketLost implements the SendAlgorithm interface. +func (b *bbrSender) OnPacketLost(number congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) { + // Do nothing. +} + +// OnRetransmissionTimeout implements the SendAlgorithm interface. +func (b *bbrSender) OnRetransmissionTimeout(packetsRetransmitted bool) { + // Do nothing. +} + +// SetMaxDatagramSize implements the SendAlgorithm interface. +func (b *bbrSender) SetMaxDatagramSize(s congestion.ByteCount) { + if s < b.maxDatagramSize { + panic(fmt.Sprintf("congestion BUG: decreased max datagram size from %d to %d", b.maxDatagramSize, s)) + } + cwndIsMinCwnd := b.congestionWindow == b.minCongestionWindow + b.maxDatagramSize = s + if cwndIsMinCwnd { + b.congestionWindow = b.minCongestionWindow + } + b.pacer.SetMaxDatagramSize(s) +} + +// InSlowStart implements the SendAlgorithmWithDebugInfos interface. +func (b *bbrSender) InSlowStart() bool { + return b.mode == bbrModeStartup +} + +// InRecovery implements the SendAlgorithmWithDebugInfos interface. +func (b *bbrSender) InRecovery() bool { + return b.recoveryState != bbrRecoveryStateNotInRecovery +} + +// GetCongestionWindow implements the SendAlgorithmWithDebugInfos interface. +func (b *bbrSender) GetCongestionWindow() congestion.ByteCount { + if b.mode == bbrModeProbeRtt { + return b.probeRttCongestionWindow() + } + + if b.InRecovery() { + return min(b.congestionWindow, b.recoveryWindow) + } + + return b.congestionWindow +} + +func (b *bbrSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) { + // Do nothing. +} + +func (b *bbrSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime time.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) { + totalBytesAckedBefore := b.sampler.TotalBytesAcked() + totalBytesLostBefore := b.sampler.TotalBytesLost() + + var isRoundStart, minRttExpired bool + var excessAcked, bytesLost congestion.ByteCount + + // The send state of the largest packet in acked_packets, unless it is + // empty. If acked_packets is empty, it's the send state of the largest + // packet in lost_packets. + var lastPacketSendState sendTimeState + + b.maybeAppLimited(priorInFlight) + + // Update bytesInFlight + b.bytesInFlight = priorInFlight + for _, p := range ackedPackets { + b.bytesInFlight -= p.BytesAcked + } + for _, p := range lostPackets { + b.bytesInFlight -= p.BytesLost + } + + if len(ackedPackets) != 0 { + lastAckedPacket := ackedPackets[len(ackedPackets)-1].PacketNumber + isRoundStart = b.updateRoundTripCounter(lastAckedPacket) + b.updateRecoveryState(lastAckedPacket, len(lostPackets) != 0, isRoundStart) + } + + sample := b.sampler.OnCongestionEvent(eventTime, + ackedPackets, lostPackets, b.maxBandwidth.GetBest(), infBandwidth, b.roundTripCount) + if sample.lastPacketSendState.isValid { + b.lastSampleIsAppLimited = sample.lastPacketSendState.isAppLimited + b.hasNoAppLimitedSample = b.hasNoAppLimitedSample || !b.lastSampleIsAppLimited + } + // Avoid updating |max_bandwidth_| if a) this is a loss-only event, or b) all + // packets in |acked_packets| did not generate valid samples. (e.g. ack of + // ack-only packets). In both cases, sampler_.total_bytes_acked() will not + // change. + if totalBytesAckedBefore != b.sampler.TotalBytesAcked() { + if !sample.sampleIsAppLimited || sample.sampleMaxBandwidth > b.maxBandwidth.GetBest() { + b.maxBandwidth.Update(sample.sampleMaxBandwidth, b.roundTripCount) + } + } + + if sample.sampleRtt != infRTT { + minRttExpired = b.maybeUpdateMinRtt(eventTime, sample.sampleRtt) + } + bytesLost = b.sampler.TotalBytesLost() - totalBytesLostBefore + + excessAcked = sample.extraAcked + lastPacketSendState = sample.lastPacketSendState + + if len(lostPackets) != 0 { + b.numLossEventsInRound++ + b.bytesLostInRound += bytesLost + } + + // Handle logic specific to PROBE_BW mode. + if b.mode == bbrModeProbeBw { + b.updateGainCyclePhase(eventTime, priorInFlight, len(lostPackets) != 0) + } + + // Handle logic specific to STARTUP and DRAIN modes. + if isRoundStart && !b.isAtFullBandwidth { + b.checkIfFullBandwidthReached(&lastPacketSendState) + } + + b.maybeExitStartupOrDrain(eventTime) + + // Handle logic specific to PROBE_RTT. + b.maybeEnterOrExitProbeRtt(eventTime, isRoundStart, minRttExpired) + + // Calculate number of packets acked and lost. + bytesAcked := b.sampler.TotalBytesAcked() - totalBytesAckedBefore + + // After the model is updated, recalculate the pacing rate and congestion + // window. + b.calculatePacingRate(bytesLost) + b.calculateCongestionWindow(bytesAcked, excessAcked) + b.calculateRecoveryWindow(bytesAcked, bytesLost) + + // Cleanup internal state. + // This is where we clean up obsolete (acked or lost) packets from the bandwidth sampler. + // The "least unacked" should actually be FirstOutstanding, but since we are not passing + // that through OnCongestionEventEx, we will only do an estimate using acked/lost packets + // for now. Because of fast retransmission, they should differ by no more than 2 packets. + // (this is controlled by packetThreshold in quic-go's sentPacketHandler) + var leastUnacked congestion.PacketNumber + if len(ackedPackets) != 0 { + leastUnacked = ackedPackets[len(ackedPackets)-1].PacketNumber - 2 + } else { + leastUnacked = lostPackets[len(lostPackets)-1].PacketNumber + 1 + } + b.sampler.RemoveObsoletePackets(leastUnacked) + + if isRoundStart { + b.numLossEventsInRound = 0 + b.bytesLostInRound = 0 + } +} + +func (b *bbrSender) PacingRate() Bandwidth { + if b.pacingRate == 0 { + return Bandwidth(b.highGain * float64( + BandwidthFromDelta(b.initialCongestionWindow, b.getMinRtt()))) + } + + return b.pacingRate +} + +func (b *bbrSender) hasGoodBandwidthEstimateForResumption() bool { + return b.hasNonAppLimitedSample() +} + +func (b *bbrSender) hasNonAppLimitedSample() bool { + return b.hasNoAppLimitedSample +} + +// Sets the pacing gain used in STARTUP. Must be greater than 1. +func (b *bbrSender) setHighGain(highGain float64) { + b.highGain = highGain + if b.mode == bbrModeStartup { + b.pacingGain = highGain + } +} + +// Sets the CWND gain used in STARTUP. Must be greater than 1. +func (b *bbrSender) setHighCwndGain(highCwndGain float64) { + b.highCwndGain = highCwndGain + if b.mode == bbrModeStartup { + b.congestionWindowGain = highCwndGain + } +} + +// Sets the gain used in DRAIN. Must be less than 1. +func (b *bbrSender) setDrainGain(drainGain float64) { + b.drainGain = drainGain +} + +// Get the current bandwidth estimate. Note that Bandwidth is in bits per second. +func (b *bbrSender) bandwidthEstimate() Bandwidth { + return b.maxBandwidth.GetBest() +} + +func (b *bbrSender) bandwidthForPacer() congestion.ByteCount { + bps := congestion.ByteCount(float64(b.bandwidthEstimate()) * b.congestionWindowGain / float64(BytesPerSecond)) + if bps < minBps { + // We need to make sure that the bandwidth value for pacer is never zero, + // otherwise it will go into an edge case where HasPacingBudget = false + // but TimeUntilSend is before, causing the quic-go send loop to go crazy and get stuck. + return minBps + } + return bps +} + +// Returns the current estimate of the RTT of the connection. Outside of the +// edge cases, this is minimum RTT. +func (b *bbrSender) getMinRtt() time.Duration { + if b.minRtt != 0 { + return b.minRtt + } + // min_rtt could be available if the handshake packet gets neutered then + // gets acknowledged. This could only happen for QUIC crypto where we do not + // drop keys. + minRtt := b.rttStats.MinRTT() + if minRtt == 0 { + return 100 * time.Millisecond + } else { + return minRtt + } +} + +// Computes the target congestion window using the specified gain. +func (b *bbrSender) getTargetCongestionWindow(gain float64) congestion.ByteCount { + bdp := bdpFromRttAndBandwidth(b.getMinRtt(), b.bandwidthEstimate()) + congestionWindow := congestion.ByteCount(gain * float64(bdp)) + + // BDP estimate will be zero if no bandwidth samples are available yet. + if congestionWindow == 0 { + congestionWindow = congestion.ByteCount(gain * float64(b.initialCongestionWindow)) + } + + return max(congestionWindow, b.minCongestionWindow) +} + +// The target congestion window during PROBE_RTT. +func (b *bbrSender) probeRttCongestionWindow() congestion.ByteCount { + return b.minCongestionWindow +} + +func (b *bbrSender) maybeUpdateMinRtt(now time.Time, sampleMinRtt time.Duration) bool { + // Do not expire min_rtt if none was ever available. + minRttExpired := b.minRtt != 0 && now.After(b.minRttTimestamp.Add(minRttExpiry)) + if minRttExpired || sampleMinRtt < b.minRtt || b.minRtt == 0 { + b.minRtt = sampleMinRtt + b.minRttTimestamp = now + } + + return minRttExpired +} + +// Enters the STARTUP mode. +func (b *bbrSender) enterStartupMode(now time.Time) { + b.mode = bbrModeStartup + // b.maybeTraceStateChange(logging.CongestionStateStartup) + b.pacingGain = b.highGain + b.congestionWindowGain = b.highCwndGain + + if b.debug { + b.debugPrint("Phase: STARTUP") + } +} + +// Enters the PROBE_BW mode. +func (b *bbrSender) enterProbeBandwidthMode(now time.Time) { + b.mode = bbrModeProbeBw + // b.maybeTraceStateChange(logging.CongestionStateProbeBw) + b.congestionWindowGain = b.congestionWindowGainConstant + + // Pick a random offset for the gain cycle out of {0, 2..7} range. 1 is + // excluded because in that case increased gain and decreased gain would not + // follow each other. + b.cycleCurrentOffset = int(rand.Int31n(congestion.PacketsPerConnectionID)) % (gainCycleLength - 1) + if b.cycleCurrentOffset >= 1 { + b.cycleCurrentOffset += 1 + } + + b.lastCycleStart = now + b.pacingGain = pacingGain[b.cycleCurrentOffset] + + if b.debug { + b.debugPrint("Phase: PROBE_BW") + } +} + +// Updates the round-trip counter if a round-trip has passed. Returns true if +// the counter has been advanced. +func (b *bbrSender) updateRoundTripCounter(lastAckedPacket congestion.PacketNumber) bool { + if b.currentRoundTripEnd == invalidPacketNumber || lastAckedPacket > b.currentRoundTripEnd { + b.roundTripCount++ + b.currentRoundTripEnd = b.lastSentPacket + return true + } + return false +} + +// Updates the current gain used in PROBE_BW mode. +func (b *bbrSender) updateGainCyclePhase(now time.Time, priorInFlight congestion.ByteCount, hasLosses bool) { + // In most cases, the cycle is advanced after an RTT passes. + shouldAdvanceGainCycling := now.After(b.lastCycleStart.Add(b.getMinRtt())) + // If the pacing gain is above 1.0, the connection is trying to probe the + // bandwidth by increasing the number of bytes in flight to at least + // pacing_gain * BDP. Make sure that it actually reaches the target, as long + // as there are no losses suggesting that the buffers are not able to hold + // that much. + if b.pacingGain > 1.0 && !hasLosses && priorInFlight < b.getTargetCongestionWindow(b.pacingGain) { + shouldAdvanceGainCycling = false + } + + // If pacing gain is below 1.0, the connection is trying to drain the extra + // queue which could have been incurred by probing prior to it. If the number + // of bytes in flight falls down to the estimated BDP value earlier, conclude + // that the queue has been successfully drained and exit this cycle early. + if b.pacingGain < 1.0 && b.bytesInFlight <= b.getTargetCongestionWindow(1) { + shouldAdvanceGainCycling = true + } + + if shouldAdvanceGainCycling { + b.cycleCurrentOffset = (b.cycleCurrentOffset + 1) % gainCycleLength + b.lastCycleStart = now + // Stay in low gain mode until the target BDP is hit. + // Low gain mode will be exited immediately when the target BDP is achieved. + if b.drainToTarget && b.pacingGain < 1 && + pacingGain[b.cycleCurrentOffset] == 1 && + b.bytesInFlight > b.getTargetCongestionWindow(1) { + return + } + b.pacingGain = pacingGain[b.cycleCurrentOffset] + } +} + +// Tracks for how many round-trips the bandwidth has not increased +// significantly. +func (b *bbrSender) checkIfFullBandwidthReached(lastPacketSendState *sendTimeState) { + if b.lastSampleIsAppLimited { + return + } + + target := Bandwidth(float64(b.bandwidthAtLastRound) * startupGrowthTarget) + if b.bandwidthEstimate() >= target { + b.bandwidthAtLastRound = b.bandwidthEstimate() + b.roundsWithoutBandwidthGain = 0 + if b.expireAckAggregationInStartup { + // Expire old excess delivery measurements now that bandwidth increased. + b.sampler.ResetMaxAckHeightTracker(0, b.roundTripCount) + } + return + } + + b.roundsWithoutBandwidthGain++ + if b.roundsWithoutBandwidthGain >= b.numStartupRtts || + b.shouldExitStartupDueToLoss(lastPacketSendState) { + b.isAtFullBandwidth = true + } +} + +func (b *bbrSender) maybeAppLimited(bytesInFlight congestion.ByteCount) { + if bytesInFlight < b.getTargetCongestionWindow(1) { + b.sampler.OnAppLimited() + } +} + +// Transitions from STARTUP to DRAIN and from DRAIN to PROBE_BW if +// appropriate. +func (b *bbrSender) maybeExitStartupOrDrain(now time.Time) { + if b.mode == bbrModeStartup && b.isAtFullBandwidth { + b.mode = bbrModeDrain + // b.maybeTraceStateChange(logging.CongestionStateDrain) + b.pacingGain = b.drainGain + b.congestionWindowGain = b.highCwndGain + + if b.debug { + b.debugPrint("Phase: DRAIN") + } + } + if b.mode == bbrModeDrain && b.bytesInFlight <= b.getTargetCongestionWindow(1) { + b.enterProbeBandwidthMode(now) + } +} + +// Decides whether to enter or exit PROBE_RTT. +func (b *bbrSender) maybeEnterOrExitProbeRtt(now time.Time, isRoundStart, minRttExpired bool) { + if minRttExpired && !b.exitingQuiescence && b.mode != bbrModeProbeRtt { + b.mode = bbrModeProbeRtt + // b.maybeTraceStateChange(logging.CongestionStateProbRtt) + b.pacingGain = 1.0 + // Do not decide on the time to exit PROBE_RTT until the |bytes_in_flight| + // is at the target small value. + b.exitProbeRttAt = time.Time{} + + if b.debug { + b.debugPrint("BandwidthEstimate: %s, CongestionWindowGain: %.2f, PacingGain: %.2f, PacingRate: %s", + formatSpeed(b.bandwidthEstimate()), b.congestionWindowGain, b.pacingGain, formatSpeed(b.PacingRate())) + b.debugPrint("Phase: PROBE_RTT") + } + } + + if b.mode == bbrModeProbeRtt { + b.sampler.OnAppLimited() + // b.maybeTraceStateChange(logging.CongestionStateApplicationLimited) + + if b.exitProbeRttAt.IsZero() { + // If the window has reached the appropriate size, schedule exiting + // PROBE_RTT. The CWND during PROBE_RTT is kMinimumCongestionWindow, but + // we allow an extra packet since QUIC checks CWND before sending a + // packet. + if b.bytesInFlight < b.probeRttCongestionWindow()+congestion.MaxPacketBufferSize { + b.exitProbeRttAt = now.Add(probeRttTime) + b.probeRttRoundPassed = false + } + } else { + if isRoundStart { + b.probeRttRoundPassed = true + } + if now.Sub(b.exitProbeRttAt) >= 0 && b.probeRttRoundPassed { + b.minRttTimestamp = now + if b.debug { + b.debugPrint("MinRTT: %s", b.getMinRtt()) + } + if !b.isAtFullBandwidth { + b.enterStartupMode(now) + } else { + b.enterProbeBandwidthMode(now) + } + } + } + } + + b.exitingQuiescence = false +} + +// Determines whether BBR needs to enter, exit or advance state of the +// recovery. +func (b *bbrSender) updateRecoveryState(lastAckedPacket congestion.PacketNumber, hasLosses, isRoundStart bool) { + // Disable recovery in startup, if loss-based exit is enabled. + if !b.isAtFullBandwidth { + return + } + + // Exit recovery when there are no losses for a round. + if hasLosses { + b.endRecoveryAt = b.lastSentPacket + } + + switch b.recoveryState { + case bbrRecoveryStateNotInRecovery: + if hasLosses { + b.recoveryState = bbrRecoveryStateConservation + // This will cause the |recovery_window_| to be set to the correct + // value in CalculateRecoveryWindow(). + b.recoveryWindow = 0 + // Since the conservation phase is meant to be lasting for a whole + // round, extend the current round as if it were started right now. + b.currentRoundTripEnd = b.lastSentPacket + } + case bbrRecoveryStateConservation: + if isRoundStart { + b.recoveryState = bbrRecoveryStateGrowth + } + fallthrough + case bbrRecoveryStateGrowth: + // Exit recovery if appropriate. + if !hasLosses && lastAckedPacket > b.endRecoveryAt { + b.recoveryState = bbrRecoveryStateNotInRecovery + } + } +} + +// Determines the appropriate pacing rate for the connection. +func (b *bbrSender) calculatePacingRate(bytesLost congestion.ByteCount) { + if b.bandwidthEstimate() == 0 { + return + } + + targetRate := Bandwidth(b.pacingGain * float64(b.bandwidthEstimate())) + if b.isAtFullBandwidth { + b.pacingRate = targetRate + return + } + + // Pace at the rate of initial_window / RTT as soon as RTT measurements are + // available. + if b.pacingRate == 0 && b.rttStats.MinRTT() != 0 { + b.pacingRate = BandwidthFromDelta(b.initialCongestionWindow, b.rttStats.MinRTT()) + return + } + + if b.detectOvershooting { + b.bytesLostWhileDetectingOvershooting += bytesLost + // Check for overshooting with network parameters adjusted when pacing rate + // > target_rate and loss has been detected. + if b.pacingRate > targetRate && b.bytesLostWhileDetectingOvershooting > 0 { + if b.hasNoAppLimitedSample || + b.bytesLostWhileDetectingOvershooting*congestion.ByteCount(b.bytesLostMultiplierWhileDetectingOvershooting) > b.initialCongestionWindow { + // We are fairly sure overshoot happens if 1) there is at least one + // non app-limited bw sample or 2) half of IW gets lost. Slow pacing + // rate. + b.pacingRate = max(targetRate, BandwidthFromDelta(b.cwndToCalculateMinPacingRate, b.rttStats.MinRTT())) + b.bytesLostWhileDetectingOvershooting = 0 + b.detectOvershooting = false + } + } + } + + // Do not decrease the pacing rate during startup. + b.pacingRate = max(b.pacingRate, targetRate) +} + +// Determines the appropriate congestion window for the connection. +func (b *bbrSender) calculateCongestionWindow(bytesAcked, excessAcked congestion.ByteCount) { + if b.mode == bbrModeProbeRtt { + return + } + + targetWindow := b.getTargetCongestionWindow(b.congestionWindowGain) + if b.isAtFullBandwidth { + // Add the max recently measured ack aggregation to CWND. + targetWindow += b.sampler.MaxAckHeight() + } else if b.enableAckAggregationDuringStartup { + // Add the most recent excess acked. Because CWND never decreases in + // STARTUP, this will automatically create a very localized max filter. + targetWindow += excessAcked + } + + // Instead of immediately setting the target CWND as the new one, BBR grows + // the CWND towards |target_window| by only increasing it |bytes_acked| at a + // time. + if b.isAtFullBandwidth { + b.congestionWindow = min(targetWindow, b.congestionWindow+bytesAcked) + } else if b.congestionWindow < targetWindow || + b.sampler.TotalBytesAcked() < b.initialCongestionWindow { + // If the connection is not yet out of startup phase, do not decrease the + // window. + b.congestionWindow += bytesAcked + } + + // Enforce the limits on the congestion window. + b.congestionWindow = max(b.congestionWindow, b.minCongestionWindow) + b.congestionWindow = min(b.congestionWindow, b.maxCongestionWindow) +} + +// Determines the appropriate window that constrains the in-flight during recovery. +func (b *bbrSender) calculateRecoveryWindow(bytesAcked, bytesLost congestion.ByteCount) { + if b.recoveryState == bbrRecoveryStateNotInRecovery { + return + } + + // Set up the initial recovery window. + if b.recoveryWindow == 0 { + b.recoveryWindow = b.bytesInFlight + bytesAcked + b.recoveryWindow = max(b.minCongestionWindow, b.recoveryWindow) + return + } + + // Remove losses from the recovery window, while accounting for a potential + // integer underflow. + if b.recoveryWindow >= bytesLost { + b.recoveryWindow = b.recoveryWindow - bytesLost + } else { + b.recoveryWindow = b.maxDatagramSize + } + + // In CONSERVATION mode, just subtracting losses is sufficient. In GROWTH, + // release additional |bytes_acked| to achieve a slow-start-like behavior. + if b.recoveryState == bbrRecoveryStateGrowth { + b.recoveryWindow += bytesAcked + } + + // Always allow sending at least |bytes_acked| in response. + b.recoveryWindow = max(b.recoveryWindow, b.bytesInFlight+bytesAcked) + b.recoveryWindow = max(b.minCongestionWindow, b.recoveryWindow) +} + +// Return whether we should exit STARTUP due to excessive loss. +func (b *bbrSender) shouldExitStartupDueToLoss(lastPacketSendState *sendTimeState) bool { + if b.numLossEventsInRound < defaultStartupFullLossCount || !lastPacketSendState.isValid { + return false + } + + inflightAtSend := lastPacketSendState.bytesInFlight + + if inflightAtSend > 0 && b.bytesLostInRound > 0 { + if b.bytesLostInRound > congestion.ByteCount(float64(inflightAtSend)*quicBbr2DefaultLossThreshold) { + return true + } + return false + } + return false +} + +func (b *bbrSender) debugPrint(format string, a ...any) { + fmt.Printf("[BBRSender] [%s] %s\n", + time.Now().Format("15:04:05"), + fmt.Sprintf(format, a...)) +} + +func bdpFromRttAndBandwidth(rtt time.Duration, bandwidth Bandwidth) congestion.ByteCount { + return congestion.ByteCount(rtt) * congestion.ByteCount(bandwidth) / congestion.ByteCount(BytesPerSecond) / congestion.ByteCount(time.Second) +} + +func GetInitialPacketSize(addr net.Addr) congestion.ByteCount { + // If this is not a UDP address, we don't know anything about the MTU. + // Use the minimum size of an Initial packet as the max packet size. + if udpAddr, ok := addr.(*net.UDPAddr); ok { + if udpAddr.IP.To4() != nil { + return congestion.InitialPacketSizeIPv4 + } else { + return congestion.InitialPacketSizeIPv6 + } + } else { + return congestion.MinInitialPacketSize + } +} + +func formatSpeed(bw Bandwidth) string { + bwf := float64(bw) + units := []string{"bps", "Kbps", "Mbps", "Gbps"} + unitIndex := 0 + for bwf > 1000 && unitIndex < len(units)-1 { + bwf /= 1000 + unitIndex++ + } + return fmt.Sprintf("%.2f %s", bwf, units[unitIndex]) +} diff --git a/v2rayng/hysteria/core/internal/congestion/bbr/clock.go b/v2rayng/hysteria/core/internal/congestion/bbr/clock.go new file mode 100644 index 0000000000..a66344fb71 --- /dev/null +++ b/v2rayng/hysteria/core/internal/congestion/bbr/clock.go @@ -0,0 +1,18 @@ +package bbr + +import "time" + +// A Clock returns the current time +type Clock interface { + Now() time.Time +} + +// DefaultClock implements the Clock interface using the Go stdlib clock. +type DefaultClock struct{} + +var _ Clock = DefaultClock{} + +// Now gets the current time +func (DefaultClock) Now() time.Time { + return time.Now() +} diff --git a/v2rayng/hysteria/core/internal/congestion/bbr/packet_number_indexed_queue.go b/v2rayng/hysteria/core/internal/congestion/bbr/packet_number_indexed_queue.go new file mode 100644 index 0000000000..86fe52d2be --- /dev/null +++ b/v2rayng/hysteria/core/internal/congestion/bbr/packet_number_indexed_queue.go @@ -0,0 +1,199 @@ +package bbr + +import ( + "github.com/apernet/quic-go/congestion" +) + +// packetNumberIndexedQueue is a queue of mostly continuous numbered entries +// which supports the following operations: +// - adding elements to the end of the queue, or at some point past the end +// - removing elements in any order +// - retrieving elements +// If all elements are inserted in order, all of the operations above are +// amortized O(1) time. +// +// Internally, the data structure is a deque where each element is marked as +// present or not. The deque starts at the lowest present index. Whenever an +// element is removed, it's marked as not present, and the front of the deque is +// cleared of elements that are not present. +// +// The tail of the queue is not cleared due to the assumption of entries being +// inserted in order, though removing all elements of the queue will return it +// to its initial state. +// +// Note that this data structure is inherently hazardous, since an addition of +// just two entries will cause it to consume all of the memory available. +// Because of that, it is not a general-purpose container and should not be used +// as one. + +type entryWrapper[T any] struct { + present bool + entry T +} + +type packetNumberIndexedQueue[T any] struct { + entries RingBuffer[entryWrapper[T]] + numberOfPresentEntries int + firstPacket congestion.PacketNumber +} + +func newPacketNumberIndexedQueue[T any](size int) *packetNumberIndexedQueue[T] { + q := &packetNumberIndexedQueue[T]{ + firstPacket: invalidPacketNumber, + } + + q.entries.Init(size) + + return q +} + +// Emplace inserts data associated |packet_number| into (or past) the end of the +// queue, filling up the missing intermediate entries as necessary. Returns +// true if the element has been inserted successfully, false if it was already +// in the queue or inserted out of order. +func (p *packetNumberIndexedQueue[T]) Emplace(packetNumber congestion.PacketNumber, entry *T) bool { + if packetNumber == invalidPacketNumber || entry == nil { + return false + } + + if p.IsEmpty() { + p.entries.PushBack(entryWrapper[T]{ + present: true, + entry: *entry, + }) + p.numberOfPresentEntries = 1 + p.firstPacket = packetNumber + return true + } + + // Do not allow insertion out-of-order. + if packetNumber <= p.LastPacket() { + return false + } + + // Handle potentially missing elements. + offset := int(packetNumber - p.FirstPacket()) + if gap := offset - p.entries.Len(); gap > 0 { + for i := 0; i < gap; i++ { + p.entries.PushBack(entryWrapper[T]{}) + } + } + + p.entries.PushBack(entryWrapper[T]{ + present: true, + entry: *entry, + }) + p.numberOfPresentEntries++ + return true +} + +// GetEntry Retrieve the entry associated with the packet number. Returns the pointer +// to the entry in case of success, or nullptr if the entry does not exist. +func (p *packetNumberIndexedQueue[T]) GetEntry(packetNumber congestion.PacketNumber) *T { + ew := p.getEntryWraper(packetNumber) + if ew == nil { + return nil + } + + return &ew.entry +} + +// Remove, Same as above, but if an entry is present in the queue, also call f(entry) +// before removing it. +func (p *packetNumberIndexedQueue[T]) Remove(packetNumber congestion.PacketNumber, f func(T)) bool { + ew := p.getEntryWraper(packetNumber) + if ew == nil { + return false + } + if f != nil { + f(ew.entry) + } + ew.present = false + p.numberOfPresentEntries-- + + if packetNumber == p.FirstPacket() { + p.clearup() + } + + return true +} + +// RemoveUpTo, but not including |packet_number|. +// Unused slots in the front are also removed, which means when the function +// returns, |first_packet()| can be larger than |packet_number|. +func (p *packetNumberIndexedQueue[T]) RemoveUpTo(packetNumber congestion.PacketNumber) { + for !p.entries.Empty() && + p.firstPacket != invalidPacketNumber && + p.firstPacket < packetNumber { + if p.entries.Front().present { + p.numberOfPresentEntries-- + } + p.entries.PopFront() + p.firstPacket++ + } + p.clearup() + + return +} + +// IsEmpty return if queue is empty. +func (p *packetNumberIndexedQueue[T]) IsEmpty() bool { + return p.numberOfPresentEntries == 0 +} + +// NumberOfPresentEntries returns the number of entries in the queue. +func (p *packetNumberIndexedQueue[T]) NumberOfPresentEntries() int { + return p.numberOfPresentEntries +} + +// EntrySlotsUsed returns the number of entries allocated in the underlying deque. This is +// proportional to the memory usage of the queue. +func (p *packetNumberIndexedQueue[T]) EntrySlotsUsed() int { + return p.entries.Len() +} + +// LastPacket returns packet number of the first entry in the queue. +func (p *packetNumberIndexedQueue[T]) FirstPacket() (packetNumber congestion.PacketNumber) { + return p.firstPacket +} + +// LastPacket returns packet number of the last entry ever inserted in the queue. Note that the +// entry in question may have already been removed. Zero if the queue is +// empty. +func (p *packetNumberIndexedQueue[T]) LastPacket() (packetNumber congestion.PacketNumber) { + if p.IsEmpty() { + return invalidPacketNumber + } + + return p.firstPacket + congestion.PacketNumber(p.entries.Len()-1) +} + +func (p *packetNumberIndexedQueue[T]) clearup() { + for !p.entries.Empty() && !p.entries.Front().present { + p.entries.PopFront() + p.firstPacket++ + } + if p.entries.Empty() { + p.firstPacket = invalidPacketNumber + } +} + +func (p *packetNumberIndexedQueue[T]) getEntryWraper(packetNumber congestion.PacketNumber) *entryWrapper[T] { + if packetNumber == invalidPacketNumber || + p.IsEmpty() || + packetNumber < p.firstPacket { + return nil + } + + offset := int(packetNumber - p.firstPacket) + if offset >= p.entries.Len() { + return nil + } + + ew := p.entries.Offset(offset) + if ew == nil || !ew.present { + return nil + } + + return ew +} diff --git a/v2rayng/hysteria/core/internal/congestion/bbr/ringbuffer.go b/v2rayng/hysteria/core/internal/congestion/bbr/ringbuffer.go new file mode 100644 index 0000000000..ed92d4ce01 --- /dev/null +++ b/v2rayng/hysteria/core/internal/congestion/bbr/ringbuffer.go @@ -0,0 +1,118 @@ +package bbr + +// A RingBuffer is a ring buffer. +// It acts as a heap that doesn't cause any allocations. +type RingBuffer[T any] struct { + ring []T + headPos, tailPos int + full bool +} + +// Init preallocs a buffer with a certain size. +func (r *RingBuffer[T]) Init(size int) { + r.ring = make([]T, size) +} + +// Len returns the number of elements in the ring buffer. +func (r *RingBuffer[T]) Len() int { + if r.full { + return len(r.ring) + } + if r.tailPos >= r.headPos { + return r.tailPos - r.headPos + } + return r.tailPos - r.headPos + len(r.ring) +} + +// Empty says if the ring buffer is empty. +func (r *RingBuffer[T]) Empty() bool { + return !r.full && r.headPos == r.tailPos +} + +// PushBack adds a new element. +// If the ring buffer is full, its capacity is increased first. +func (r *RingBuffer[T]) PushBack(t T) { + if r.full || len(r.ring) == 0 { + r.grow() + } + r.ring[r.tailPos] = t + r.tailPos++ + if r.tailPos == len(r.ring) { + r.tailPos = 0 + } + if r.tailPos == r.headPos { + r.full = true + } +} + +// PopFront returns the next element. +// It must not be called when the buffer is empty, that means that +// callers might need to check if there are elements in the buffer first. +func (r *RingBuffer[T]) PopFront() T { + if r.Empty() { + panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: pop from an empty queue") + } + r.full = false + t := r.ring[r.headPos] + r.ring[r.headPos] = *new(T) + r.headPos++ + if r.headPos == len(r.ring) { + r.headPos = 0 + } + return t +} + +// Offset returns the offset element. +// It must not be called when the buffer is empty, that means that +// callers might need to check if there are elements in the buffer first +// and check if the index larger than buffer length. +func (r *RingBuffer[T]) Offset(index int) *T { + if r.Empty() || index >= r.Len() { + panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: offset from invalid index") + } + offset := (r.headPos + index) % len(r.ring) + return &r.ring[offset] +} + +// Front returns the front element. +// It must not be called when the buffer is empty, that means that +// callers might need to check if there are elements in the buffer first. +func (r *RingBuffer[T]) Front() *T { + if r.Empty() { + panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: front from an empty queue") + } + return &r.ring[r.headPos] +} + +// Back returns the back element. +// It must not be called when the buffer is empty, that means that +// callers might need to check if there are elements in the buffer first. +func (r *RingBuffer[T]) Back() *T { + if r.Empty() { + panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: back from an empty queue") + } + return r.Offset(r.Len() - 1) +} + +// Grow the maximum size of the queue. +// This method assume the queue is full. +func (r *RingBuffer[T]) grow() { + oldRing := r.ring + newSize := len(oldRing) * 2 + if newSize == 0 { + newSize = 1 + } + r.ring = make([]T, newSize) + headLen := copy(r.ring, oldRing[r.headPos:]) + copy(r.ring[headLen:], oldRing[:r.headPos]) + r.headPos, r.tailPos, r.full = 0, len(oldRing), false +} + +// Clear removes all elements. +func (r *RingBuffer[T]) Clear() { + var zeroValue T + for i := range r.ring { + r.ring[i] = zeroValue + } + r.headPos, r.tailPos, r.full = 0, 0, false +} diff --git a/v2rayng/hysteria/core/internal/congestion/bbr/windowed_filter.go b/v2rayng/hysteria/core/internal/congestion/bbr/windowed_filter.go new file mode 100644 index 0000000000..4773bce597 --- /dev/null +++ b/v2rayng/hysteria/core/internal/congestion/bbr/windowed_filter.go @@ -0,0 +1,162 @@ +package bbr + +import ( + "golang.org/x/exp/constraints" +) + +// Implements Kathleen Nichols' algorithm for tracking the minimum (or maximum) +// estimate of a stream of samples over some fixed time interval. (E.g., +// the minimum RTT over the past five minutes.) The algorithm keeps track of +// the best, second best, and third best min (or max) estimates, maintaining an +// invariant that the measurement time of the n'th best >= n-1'th best. + +// The algorithm works as follows. On a reset, all three estimates are set to +// the same sample. The second best estimate is then recorded in the second +// quarter of the window, and a third best estimate is recorded in the second +// half of the window, bounding the worst case error when the true min is +// monotonically increasing (or true max is monotonically decreasing) over the +// window. +// +// A new best sample replaces all three estimates, since the new best is lower +// (or higher) than everything else in the window and it is the most recent. +// The window thus effectively gets reset on every new min. The same property +// holds true for second best and third best estimates. Specifically, when a +// sample arrives that is better than the second best but not better than the +// best, it replaces the second and third best estimates but not the best +// estimate. Similarly, a sample that is better than the third best estimate +// but not the other estimates replaces only the third best estimate. +// +// Finally, when the best expires, it is replaced by the second best, which in +// turn is replaced by the third best. The newest sample replaces the third +// best. + +type WindowedFilterValue interface { + any +} + +type WindowedFilterTime interface { + constraints.Integer | constraints.Float +} + +type WindowedFilter[V WindowedFilterValue, T WindowedFilterTime] struct { + // Time length of window. + windowLength T + estimates []entry[V, T] + comparator func(V, V) int +} + +type entry[V WindowedFilterValue, T WindowedFilterTime] struct { + sample V + time T +} + +// Compares two values and returns true if the first is greater than or equal +// to the second. +func MaxFilter[O constraints.Ordered](a, b O) int { + if a > b { + return 1 + } else if a < b { + return -1 + } + return 0 +} + +// Compares two values and returns true if the first is less than or equal +// to the second. +func MinFilter[O constraints.Ordered](a, b O) int { + if a < b { + return 1 + } else if a > b { + return -1 + } + return 0 +} + +func NewWindowedFilter[V WindowedFilterValue, T WindowedFilterTime](windowLength T, comparator func(V, V) int) *WindowedFilter[V, T] { + return &WindowedFilter[V, T]{ + windowLength: windowLength, + estimates: make([]entry[V, T], 3, 3), + comparator: comparator, + } +} + +// Changes the window length. Does not update any current samples. +func (f *WindowedFilter[V, T]) SetWindowLength(windowLength T) { + f.windowLength = windowLength +} + +func (f *WindowedFilter[V, T]) GetBest() V { + return f.estimates[0].sample +} + +func (f *WindowedFilter[V, T]) GetSecondBest() V { + return f.estimates[1].sample +} + +func (f *WindowedFilter[V, T]) GetThirdBest() V { + return f.estimates[2].sample +} + +// Updates best estimates with |sample|, and expires and updates best +// estimates as necessary. +func (f *WindowedFilter[V, T]) Update(newSample V, newTime T) { + // Reset all estimates if they have not yet been initialized, if new sample + // is a new best, or if the newest recorded estimate is too old. + if f.comparator(f.estimates[0].sample, *new(V)) == 0 || + f.comparator(newSample, f.estimates[0].sample) >= 0 || + newTime-f.estimates[2].time > f.windowLength { + f.Reset(newSample, newTime) + return + } + + if f.comparator(newSample, f.estimates[1].sample) >= 0 { + f.estimates[1] = entry[V, T]{newSample, newTime} + f.estimates[2] = f.estimates[1] + } else if f.comparator(newSample, f.estimates[2].sample) >= 0 { + f.estimates[2] = entry[V, T]{newSample, newTime} + } + + // Expire and update estimates as necessary. + if newTime-f.estimates[0].time > f.windowLength { + // The best estimate hasn't been updated for an entire window, so promote + // second and third best estimates. + f.estimates[0] = f.estimates[1] + f.estimates[1] = f.estimates[2] + f.estimates[2] = entry[V, T]{newSample, newTime} + // Need to iterate one more time. Check if the new best estimate is + // outside the window as well, since it may also have been recorded a + // long time ago. Don't need to iterate once more since we cover that + // case at the beginning of the method. + if newTime-f.estimates[0].time > f.windowLength { + f.estimates[0] = f.estimates[1] + f.estimates[1] = f.estimates[2] + } + return + } + if f.comparator(f.estimates[1].sample, f.estimates[0].sample) == 0 && + newTime-f.estimates[1].time > f.windowLength/4 { + // A quarter of the window has passed without a better sample, so the + // second-best estimate is taken from the second quarter of the window. + f.estimates[1] = entry[V, T]{newSample, newTime} + f.estimates[2] = f.estimates[1] + return + } + + if f.comparator(f.estimates[2].sample, f.estimates[1].sample) == 0 && + newTime-f.estimates[2].time > f.windowLength/2 { + // We've passed a half of the window without a better estimate, so take + // a third-best estimate from the second half of the window. + f.estimates[2] = entry[V, T]{newSample, newTime} + } +} + +// Resets all estimates to new sample. +func (f *WindowedFilter[V, T]) Reset(newSample V, newTime T) { + f.estimates[2] = entry[V, T]{newSample, newTime} + f.estimates[1] = f.estimates[2] + f.estimates[0] = f.estimates[1] +} + +func (f *WindowedFilter[V, T]) Clear() { + f.estimates = make([]entry[V, T], 3, 3) +} diff --git a/v2rayng/hysteria/core/internal/congestion/brutal/brutal.go b/v2rayng/hysteria/core/internal/congestion/brutal/brutal.go new file mode 100644 index 0000000000..de591a94af --- /dev/null +++ b/v2rayng/hysteria/core/internal/congestion/brutal/brutal.go @@ -0,0 +1,185 @@ +package brutal + +import ( + "fmt" + "os" + "strconv" + "time" + + "github.com/apernet/hysteria/core/v2/internal/congestion/common" + + "github.com/apernet/quic-go/congestion" +) + +const ( + pktInfoSlotCount = 5 // slot index is based on seconds, so this is basically how many seconds we sample + minSampleCount = 50 + minAckRate = 0.8 + congestionWindowMultiplier = 2 + + debugEnv = "HYSTERIA_BRUTAL_DEBUG" + debugPrintInterval = 2 +) + +var _ congestion.CongestionControl = &BrutalSender{} + +type BrutalSender struct { + rttStats congestion.RTTStatsProvider + bps congestion.ByteCount + maxDatagramSize congestion.ByteCount + pacer *common.Pacer + + pktInfoSlots [pktInfoSlotCount]pktInfo + ackRate float64 + + debug bool + lastAckPrintTimestamp int64 +} + +type pktInfo struct { + Timestamp int64 + AckCount uint64 + LossCount uint64 +} + +func NewBrutalSender(bps uint64) *BrutalSender { + debug, _ := strconv.ParseBool(os.Getenv(debugEnv)) + bs := &BrutalSender{ + bps: congestion.ByteCount(bps), + maxDatagramSize: congestion.InitialPacketSizeIPv4, + ackRate: 1, + debug: debug, + } + bs.pacer = common.NewPacer(func() congestion.ByteCount { + return congestion.ByteCount(float64(bs.bps) / bs.ackRate) + }) + return bs +} + +func (b *BrutalSender) SetRTTStatsProvider(rttStats congestion.RTTStatsProvider) { + b.rttStats = rttStats +} + +func (b *BrutalSender) TimeUntilSend(bytesInFlight congestion.ByteCount) time.Time { + return b.pacer.TimeUntilSend() +} + +func (b *BrutalSender) HasPacingBudget(now time.Time) bool { + return b.pacer.Budget(now) >= b.maxDatagramSize +} + +func (b *BrutalSender) CanSend(bytesInFlight congestion.ByteCount) bool { + return bytesInFlight <= b.GetCongestionWindow() +} + +func (b *BrutalSender) GetCongestionWindow() congestion.ByteCount { + rtt := b.rttStats.SmoothedRTT() + if rtt <= 0 { + return 10240 + } + cwnd := congestion.ByteCount(float64(b.bps) * rtt.Seconds() * congestionWindowMultiplier / b.ackRate) + if cwnd < b.maxDatagramSize { + cwnd = b.maxDatagramSize + } + return cwnd +} + +func (b *BrutalSender) OnPacketSent(sentTime time.Time, bytesInFlight congestion.ByteCount, + packetNumber congestion.PacketNumber, bytes congestion.ByteCount, isRetransmittable bool, +) { + b.pacer.SentPacket(sentTime, bytes) +} + +func (b *BrutalSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes congestion.ByteCount, + priorInFlight congestion.ByteCount, eventTime time.Time, +) { + // Stub +} + +func (b *BrutalSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes congestion.ByteCount, + priorInFlight congestion.ByteCount, +) { + // Stub +} + +func (b *BrutalSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime time.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) { + currentTimestamp := eventTime.Unix() + slot := currentTimestamp % pktInfoSlotCount + if b.pktInfoSlots[slot].Timestamp == currentTimestamp { + b.pktInfoSlots[slot].LossCount += uint64(len(lostPackets)) + b.pktInfoSlots[slot].AckCount += uint64(len(ackedPackets)) + } else { + // uninitialized slot or too old, reset + b.pktInfoSlots[slot].Timestamp = currentTimestamp + b.pktInfoSlots[slot].AckCount = uint64(len(ackedPackets)) + b.pktInfoSlots[slot].LossCount = uint64(len(lostPackets)) + } + b.updateAckRate(currentTimestamp) +} + +func (b *BrutalSender) SetMaxDatagramSize(size congestion.ByteCount) { + b.maxDatagramSize = size + b.pacer.SetMaxDatagramSize(size) + if b.debug { + b.debugPrint("SetMaxDatagramSize: %d", size) + } +} + +func (b *BrutalSender) updateAckRate(currentTimestamp int64) { + minTimestamp := currentTimestamp - pktInfoSlotCount + var ackCount, lossCount uint64 + for _, info := range b.pktInfoSlots { + if info.Timestamp < minTimestamp { + continue + } + ackCount += info.AckCount + lossCount += info.LossCount + } + if ackCount+lossCount < minSampleCount { + b.ackRate = 1 + if b.canPrintAckRate(currentTimestamp) { + b.lastAckPrintTimestamp = currentTimestamp + b.debugPrint("Not enough samples (total=%d, ack=%d, loss=%d, rtt=%d)", + ackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds()) + } + return + } + rate := float64(ackCount) / float64(ackCount+lossCount) + if rate < minAckRate { + b.ackRate = minAckRate + if b.canPrintAckRate(currentTimestamp) { + b.lastAckPrintTimestamp = currentTimestamp + b.debugPrint("ACK rate too low: %.2f, clamped to %.2f (total=%d, ack=%d, loss=%d, rtt=%d)", + rate, minAckRate, ackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds()) + } + return + } + b.ackRate = rate + if b.canPrintAckRate(currentTimestamp) { + b.lastAckPrintTimestamp = currentTimestamp + b.debugPrint("ACK rate: %.2f (total=%d, ack=%d, loss=%d, rtt=%d)", + rate, ackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds()) + } +} + +func (b *BrutalSender) InSlowStart() bool { + return false +} + +func (b *BrutalSender) InRecovery() bool { + return false +} + +func (b *BrutalSender) MaybeExitSlowStart() {} + +func (b *BrutalSender) OnRetransmissionTimeout(packetsRetransmitted bool) {} + +func (b *BrutalSender) canPrintAckRate(currentTimestamp int64) bool { + return b.debug && currentTimestamp-b.lastAckPrintTimestamp >= debugPrintInterval +} + +func (b *BrutalSender) debugPrint(format string, a ...any) { + fmt.Printf("[BrutalSender] [%s] %s\n", + time.Now().Format("15:04:05"), + fmt.Sprintf(format, a...)) +} diff --git a/v2rayng/hysteria/core/internal/congestion/common/pacer.go b/v2rayng/hysteria/core/internal/congestion/common/pacer.go new file mode 100644 index 0000000000..6adbd813c2 --- /dev/null +++ b/v2rayng/hysteria/core/internal/congestion/common/pacer.go @@ -0,0 +1,79 @@ +package common + +import ( + "time" + + "github.com/apernet/quic-go/congestion" +) + +const ( + maxBurstPackets = 10 + maxBurstPacingDelayMultiplier = 4 +) + +// Pacer implements a token bucket pacing algorithm. +type Pacer struct { + budgetAtLastSent congestion.ByteCount + maxDatagramSize congestion.ByteCount + lastSentTime time.Time + getBandwidth func() congestion.ByteCount // in bytes/s +} + +func NewPacer(getBandwidth func() congestion.ByteCount) *Pacer { + p := &Pacer{ + budgetAtLastSent: maxBurstPackets * congestion.InitialPacketSizeIPv4, + maxDatagramSize: congestion.InitialPacketSizeIPv4, + getBandwidth: getBandwidth, + } + return p +} + +func (p *Pacer) SentPacket(sendTime time.Time, size congestion.ByteCount) { + budget := p.Budget(sendTime) + if size > budget { + p.budgetAtLastSent = 0 + } else { + p.budgetAtLastSent = budget - size + } + p.lastSentTime = sendTime +} + +func (p *Pacer) Budget(now time.Time) congestion.ByteCount { + if p.lastSentTime.IsZero() { + return p.maxBurstSize() + } + budget := p.budgetAtLastSent + (p.getBandwidth()*congestion.ByteCount(now.Sub(p.lastSentTime).Nanoseconds()))/1e9 + if budget < 0 { // protect against overflows + budget = congestion.ByteCount(1<<62 - 1) + } + return min(p.maxBurstSize(), budget) +} + +func (p *Pacer) maxBurstSize() congestion.ByteCount { + return max( + congestion.ByteCount((maxBurstPacingDelayMultiplier*congestion.MinPacingDelay).Nanoseconds())*p.getBandwidth()/1e9, + maxBurstPackets*p.maxDatagramSize, + ) +} + +// TimeUntilSend returns when the next packet should be sent. +// It returns the zero value of time.Time if a packet can be sent immediately. +func (p *Pacer) TimeUntilSend() time.Time { + if p.budgetAtLastSent >= p.maxDatagramSize { + return time.Time{} + } + diff := 1e9 * uint64(p.maxDatagramSize-p.budgetAtLastSent) + bw := uint64(p.getBandwidth()) + // We might need to round up this value. + // Otherwise, we might have a budget (slightly) smaller than the datagram size when the timer expires. + d := diff / bw + // this is effectively a math.Ceil, but using only integer math + if diff%bw > 0 { + d++ + } + return p.lastSentTime.Add(max(congestion.MinPacingDelay, time.Duration(d)*time.Nanosecond)) +} + +func (p *Pacer) SetMaxDatagramSize(s congestion.ByteCount) { + p.maxDatagramSize = s +} diff --git a/v2rayng/hysteria/core/internal/congestion/utils.go b/v2rayng/hysteria/core/internal/congestion/utils.go new file mode 100644 index 0000000000..1e06060aca --- /dev/null +++ b/v2rayng/hysteria/core/internal/congestion/utils.go @@ -0,0 +1,18 @@ +package congestion + +import ( + "github.com/apernet/hysteria/core/v2/internal/congestion/bbr" + "github.com/apernet/hysteria/core/v2/internal/congestion/brutal" + "github.com/apernet/quic-go" +) + +func UseBBR(conn quic.Connection) { + conn.SetCongestionControl(bbr.NewBbrSender( + bbr.DefaultClock{}, + bbr.GetInitialPacketSize(conn.RemoteAddr()), + )) +} + +func UseBrutal(conn quic.Connection, tx uint64) { + conn.SetCongestionControl(brutal.NewBrutalSender(tx)) +} diff --git a/v2rayng/hysteria/core/internal/frag/frag.go b/v2rayng/hysteria/core/internal/frag/frag.go new file mode 100644 index 0000000000..237ba0663c --- /dev/null +++ b/v2rayng/hysteria/core/internal/frag/frag.go @@ -0,0 +1,77 @@ +package frag + +import ( + "github.com/apernet/hysteria/core/v2/internal/protocol" +) + +func FragUDPMessage(m *protocol.UDPMessage, maxSize int) []protocol.UDPMessage { + if m.Size() <= maxSize { + return []protocol.UDPMessage{*m} + } + fullPayload := m.Data + maxPayloadSize := maxSize - m.HeaderSize() + off := 0 + fragID := uint8(0) + fragCount := uint8((len(fullPayload) + maxPayloadSize - 1) / maxPayloadSize) // round up + frags := make([]protocol.UDPMessage, fragCount) + for off < len(fullPayload) { + payloadSize := len(fullPayload) - off + if payloadSize > maxPayloadSize { + payloadSize = maxPayloadSize + } + frag := *m + frag.FragID = fragID + frag.FragCount = fragCount + frag.Data = fullPayload[off : off+payloadSize] + frags[fragID] = frag + off += payloadSize + fragID++ + } + return frags +} + +// Defragger handles the defragmentation of UDP messages. +// The current implementation can only handle one packet ID at a time. +// If another packet arrives before a packet has received all fragments +// in their entirety, any previous state is discarded. +type Defragger struct { + pktID uint16 + frags []*protocol.UDPMessage + count uint8 + size int // data size +} + +func (d *Defragger) Feed(m *protocol.UDPMessage) *protocol.UDPMessage { + if m.FragCount <= 1 { + return m + } + if m.FragID >= m.FragCount { + // wtf is this? + return nil + } + if m.PacketID != d.pktID || m.FragCount != uint8(len(d.frags)) { + // new message, clear previous state + d.pktID = m.PacketID + d.frags = make([]*protocol.UDPMessage, m.FragCount) + d.frags[m.FragID] = m + d.count = 1 + d.size = len(m.Data) + } else if d.frags[m.FragID] == nil { + d.frags[m.FragID] = m + d.count++ + d.size += len(m.Data) + if int(d.count) == len(d.frags) { + // all fragments received, assemble + data := make([]byte, d.size) + off := 0 + for _, frag := range d.frags { + off += copy(data[off:], frag.Data) + } + m.Data = data + m.FragID = 0 + m.FragCount = 1 + return m + } + } + return nil +} diff --git a/v2rayng/hysteria/core/internal/frag/frag_test.go b/v2rayng/hysteria/core/internal/frag/frag_test.go new file mode 100644 index 0000000000..0dc0cd7bf9 --- /dev/null +++ b/v2rayng/hysteria/core/internal/frag/frag_test.go @@ -0,0 +1,336 @@ +package frag + +import ( + "reflect" + "testing" + + "github.com/apernet/hysteria/core/v2/internal/protocol" +) + +func TestFragUDPMessage(t *testing.T) { + type args struct { + m *protocol.UDPMessage + maxSize int + } + tests := []struct { + name string + args args + want []protocol.UDPMessage + }{ + { + "no frag", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 123, + FragID: 0, + FragCount: 1, + Addr: "test:123", + Data: []byte("hello"), + }, + 100, + }, + []protocol.UDPMessage{ + { + SessionID: 123, + PacketID: 123, + FragID: 0, + FragCount: 1, + Addr: "test:123", + Data: []byte("hello"), + }, + }, + }, + { + "2 frags", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 123, + FragID: 0, + FragCount: 1, + Addr: "test:123", + Data: []byte("hello"), + }, + 20, + }, + []protocol.UDPMessage{ + { + SessionID: 123, + PacketID: 123, + FragID: 0, + FragCount: 2, + Addr: "test:123", + Data: []byte("hel"), + }, + { + SessionID: 123, + PacketID: 123, + FragID: 1, + FragCount: 2, + Addr: "test:123", + Data: []byte("lo"), + }, + }, + }, + { + "4 frags", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 123, + FragID: 0, + FragCount: 1, + Addr: "test:123", + Data: []byte("abcdefgh"), + }, + 19, + }, + []protocol.UDPMessage{ + { + SessionID: 123, + PacketID: 123, + FragID: 0, + FragCount: 4, + Addr: "test:123", + Data: []byte("ab"), + }, + { + SessionID: 123, + PacketID: 123, + FragID: 1, + FragCount: 4, + Addr: "test:123", + Data: []byte("cd"), + }, + { + SessionID: 123, + PacketID: 123, + FragID: 2, + FragCount: 4, + Addr: "test:123", + Data: []byte("ef"), + }, + { + SessionID: 123, + PacketID: 123, + FragID: 3, + FragCount: 4, + Addr: "test:123", + Data: []byte("gh"), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := FragUDPMessage(tt.args.m, tt.args.maxSize); !reflect.DeepEqual(got, tt.want) { + t.Errorf("FragUDPMessage() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDefragger(t *testing.T) { + type args struct { + m *protocol.UDPMessage + } + tests := []struct { + name string + args args + want *protocol.UDPMessage + }{ + { + "no frag", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 987, + FragID: 0, + FragCount: 1, + Addr: "test:123", + Data: []byte("hello"), + }, + }, + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 987, + FragID: 0, + FragCount: 1, + Addr: "test:123", + Data: []byte("hello"), + }, + }, + { + "frag 0 - 1/2", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 987, + FragID: 0, + FragCount: 2, + Addr: "test:123", + Data: []byte("hello "), + }, + }, + nil, + }, + { + "frag 0 - 2/2", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 987, + FragID: 1, + FragCount: 2, + Addr: "test:123", + Data: []byte("moto"), + }, + }, + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 987, + FragID: 0, + FragCount: 1, + Addr: "test:123", + Data: []byte("hello moto"), + }, + }, + { + "frag 1 - 1/3", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 987, + FragID: 0, + FragCount: 3, + Addr: "test:123", + Data: []byte("deco"), + }, + }, + nil, + }, + { + "frag 1 - 2/3", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 987, + FragID: 1, + FragCount: 3, + Addr: "test:123", + Data: []byte("*"), + }, + }, + nil, + }, + { + "frag 1 - 3/3", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 987, + FragID: 2, + FragCount: 3, + Addr: "test:123", + Data: []byte("27"), + }, + }, + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 987, + FragID: 0, + FragCount: 1, + Addr: "test:123", + Data: []byte("deco*27"), + }, + }, + { + "frag 2 - 1/2", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 233, + FragID: 1, + FragCount: 2, + Addr: "test:123", + Data: []byte("shinsekai"), + }, + }, + nil, + }, + { + "frag 3 - 2/2", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 244, + FragID: 1, + FragCount: 2, + Addr: "test:123", + Data: []byte("what???"), + }, + }, + nil, + }, + { + "frag 2 - 2/2", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 233, + FragID: 1, + FragCount: 2, + Addr: "test:123", + Data: []byte(" annaijo"), + }, + }, + nil, + }, + { + "invalid id", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 233, + FragID: 88, + FragCount: 2, + Addr: "test:123", + Data: []byte("shinsekai"), + }, + }, + nil, + }, + { + "frag 2 - 1/2 re", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 233, + FragID: 0, + FragCount: 2, + Addr: "test:123", + Data: []byte("shinsekai"), + }, + }, + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 233, + FragID: 0, + FragCount: 1, + Addr: "test:123", + Data: []byte("shinsekai annaijo"), + }, + }, + } + + d := &Defragger{} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := d.Feed(tt.args.m); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Feed() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/v2rayng/hysteria/core/internal/integration_tests/.mockery.yaml b/v2rayng/hysteria/core/internal/integration_tests/.mockery.yaml new file mode 100644 index 0000000000..550a72575a --- /dev/null +++ b/v2rayng/hysteria/core/internal/integration_tests/.mockery.yaml @@ -0,0 +1,29 @@ +with-expecter: true +dir: mocks +outpkg: mocks +packages: + net: + interfaces: + Conn: + config: + mockname: MockConn + github.com/apernet/hysteria/core/v2/server: + interfaces: + Outbound: + config: + mockname: MockOutbound + UDPConn: + config: + mockname: MockUDPConn + Authenticator: + config: + mockname: MockAuthenticator + EventLogger: + config: + mockname: MockEventLogger + TrafficLogger: + config: + mockname: MockTrafficLogger + RequestHook: + config: + mockname: MockRequestHook \ No newline at end of file diff --git a/v2rayng/hysteria/core/internal/integration_tests/close_test.go b/v2rayng/hysteria/core/internal/integration_tests/close_test.go new file mode 100644 index 0000000000..ac7f84b80a --- /dev/null +++ b/v2rayng/hysteria/core/internal/integration_tests/close_test.go @@ -0,0 +1,252 @@ +package integration_tests + +import ( + "io" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/apernet/hysteria/core/v2/client" + "github.com/apernet/hysteria/core/v2/errors" + "github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks" + "github.com/apernet/hysteria/core/v2/server" +) + +// TestClientServerTCPClose tests whether the client/server propagates the close of a connection correctly. +// Closing one side of the connection should close the other side as well. +func TestClientServerTCPClose(t *testing.T) { + // Create server + udpConn, udpAddr, err := serverConn() + assert.NoError(t, err) + serverOb := mocks.NewMockOutbound(t) + auth := mocks.NewMockAuthenticator(t) + auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Outbound: serverOb, + Authenticator: auth, + }) + assert.NoError(t, err) + defer s.Close() + go s.Serve() + + // Create client + c, _, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + }) + assert.NoError(t, err) + defer c.Close() + + addr := "hi-and-goodbye:2333" + + // Test close from client side: + // Client creates a connection, writes something, then closes it. + // Server outbound connection should write the same thing, then close. + sobConn := mocks.NewMockConn(t) + sobConnCh := make(chan struct{}) // For close signal only + sobConnChCloseFunc := sync.OnceFunc(func() { close(sobConnCh) }) + sobConn.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) { + <-sobConnCh + return 0, io.EOF + }) + sobConn.EXPECT().Write([]byte("happy")).Return(5, nil) + sobConn.EXPECT().Close().RunAndReturn(func() error { + sobConnChCloseFunc() + return nil + }) + serverOb.EXPECT().TCP(addr).Return(sobConn, nil).Once() + conn, err := c.TCP(addr) + assert.NoError(t, err) + _, err = conn.Write([]byte("happy")) + assert.NoError(t, err) + err = conn.Close() + assert.NoError(t, err) + time.Sleep(1 * time.Second) + mock.AssertExpectationsForObjects(t, sobConn, serverOb) + + // Test close from server side: + // Client creates a connection. + // Server outbound connection reads something, then closes. + // Client connection should read the same thing, then close. + sobConn = mocks.NewMockConn(t) + sobConnCh2 := make(chan []byte, 1) + sobConn.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) { + d := <-sobConnCh2 + if d == nil { + return 0, io.EOF + } else { + return copy(bs, d), nil + } + }) + sobConn.EXPECT().Close().Return(nil) + serverOb.EXPECT().TCP(addr).Return(sobConn, nil).Once() + conn, err = c.TCP(addr) + assert.NoError(t, err) + sobConnCh2 <- []byte("happy") + close(sobConnCh2) + bs, err := io.ReadAll(conn) + assert.NoError(t, err) + assert.Equal(t, "happy", string(bs)) +} + +// TestClientServerUDPIdleTimeout tests whether the server's UDP idle timeout works correctly. +func TestClientServerUDPIdleTimeout(t *testing.T) { + // Create server + udpConn, udpAddr, err := serverConn() + assert.NoError(t, err) + serverOb := mocks.NewMockOutbound(t) + auth := mocks.NewMockAuthenticator(t) + auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") + eventLogger := mocks.NewMockEventLogger(t) + eventLogger.EXPECT().Connect(mock.Anything, "nobody", mock.Anything).Once() + eventLogger.EXPECT().Disconnect(mock.Anything, "nobody", mock.Anything).Maybe() // Depends on the timing, don't care + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Outbound: serverOb, + UDPIdleTimeout: 2 * time.Second, + Authenticator: auth, + EventLogger: eventLogger, + }) + assert.NoError(t, err) + defer s.Close() + go s.Serve() + + // Create client + c, _, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + }) + assert.NoError(t, err) + defer c.Close() + + addr := "spy.x.family:2023" + + // On the client side, create a UDP session and send a packet every 1 second, + // 4 packets in total. The server should have one UDP session and receive all + // 4 packets. Then the UDP connection on the server side will receive a packet + // every 1 second, 4 packets in total. The client session should receive all + // 4 packets. Then the session will be idle for 3 seconds - should be enough + // to trigger the server's UDP idle timeout. + sobConn := mocks.NewMockUDPConn(t) + sobConnCh := make(chan []byte, 1) + sobConnChCloseFunc := sync.OnceFunc(func() { close(sobConnCh) }) + sobConn.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(bs []byte) (int, string, error) { + d := <-sobConnCh + if d == nil { + return 0, "", io.EOF + } else { + return copy(bs, d), addr, nil + } + }) + sobConn.EXPECT().WriteTo([]byte("happy"), addr).Return(5, nil).Times(4) + serverOb.EXPECT().UDP(addr).Return(sobConn, nil).Once() + eventLogger.EXPECT().UDPRequest(mock.Anything, mock.Anything, uint32(1), addr).Once() + cu, err := c.UDP() + assert.NoError(t, err) + // Client sends 4 packets + for i := 0; i < 4; i++ { + err = cu.Send([]byte("happy"), addr) + assert.NoError(t, err) + time.Sleep(1 * time.Second) + } + // Client receives 4 packets + go func() { + for i := 0; i < 4; i++ { + sobConnCh <- []byte("sad") + time.Sleep(1 * time.Second) + } + }() + for i := 0; i < 4; i++ { + bs, rAddr, err := cu.Receive() + assert.NoError(t, err) + assert.Equal(t, "sad", string(bs)) + assert.Equal(t, addr, rAddr) + } + // Now we wait for 3 seconds, the server should close the UDP session. + sobConn.EXPECT().Close().RunAndReturn(func() error { + sobConnChCloseFunc() + return nil + }) + eventLogger.EXPECT().UDPError(mock.Anything, mock.Anything, uint32(1), nil).Once() + time.Sleep(3 * time.Second) +} + +// TestClientServerClientShutdown tests whether the server can handle the client's shutdown correctly. +func TestClientServerClientShutdown(t *testing.T) { + // Create server + udpConn, udpAddr, err := serverConn() + assert.NoError(t, err) + auth := mocks.NewMockAuthenticator(t) + auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") + eventLogger := mocks.NewMockEventLogger(t) + eventLogger.EXPECT().Connect(mock.Anything, "nobody", mock.Anything).Once() + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Authenticator: auth, + EventLogger: eventLogger, + }) + assert.NoError(t, err) + defer s.Close() + go s.Serve() + + // Create client + c, _, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + }) + assert.NoError(t, err) + + // Close the client - expect disconnect event on the server side. + // Since client.Close() sends HTTP3 ErrCodeNoError, the error should be nil. + eventLogger.EXPECT().Disconnect(mock.Anything, "nobody", nil).Once() + _ = c.Close() + time.Sleep(1 * time.Second) +} + +// TestClientServerServerShutdown tests whether the client can handle the server's shutdown correctly. +func TestClientServerServerShutdown(t *testing.T) { + // Create server + udpConn, udpAddr, err := serverConn() + assert.NoError(t, err) + auth := mocks.NewMockAuthenticator(t) + auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Authenticator: auth, + }) + assert.NoError(t, err) + go s.Serve() + + // Create client + c, _, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + QUICConfig: client.QUICConfig{ + MaxIdleTimeout: 4 * time.Second, + }, + }) + assert.NoError(t, err) + + // Close the server - expect the client to return ClosedError for both TCP & UDP calls. + _ = s.Close() + + _, err = c.TCP("whatever") + _, ok := err.(errors.ClosedError) + assert.True(t, ok) + + time.Sleep(1 * time.Second) // Allow some time for the error to be propagated to the UDP session manager + + _, err = c.UDP() + _, ok = err.(errors.ClosedError) + assert.True(t, ok) + + assert.NoError(t, c.Close()) +} diff --git a/v2rayng/hysteria/core/internal/integration_tests/hook_test.go b/v2rayng/hysteria/core/internal/integration_tests/hook_test.go new file mode 100644 index 0000000000..64affe8754 --- /dev/null +++ b/v2rayng/hysteria/core/internal/integration_tests/hook_test.go @@ -0,0 +1,147 @@ +package integration_tests + +import ( + "io" + "net" + "testing" + + "github.com/apernet/hysteria/core/v2/client" + "github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks" + "github.com/apernet/hysteria/core/v2/server" + "github.com/apernet/quic-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestClientServerHookTCP(t *testing.T) { + fakeEchoAddr := "hahanope:6666" + realEchoAddr := "127.0.0.1:22333" + + // Create server + udpConn, udpAddr, err := serverConn() + assert.NoError(t, err) + auth := mocks.NewMockAuthenticator(t) + auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") + hook := mocks.NewMockRequestHook(t) + hook.EXPECT().Check(false, fakeEchoAddr).Return(true).Once() + hook.EXPECT().TCP(mock.Anything, mock.Anything).RunAndReturn(func(stream quic.Stream, s *string) ([]byte, error) { + assert.Equal(t, fakeEchoAddr, *s) + // Change the address + *s = realEchoAddr + // Read the first 5 bytes and replace them with "byeee" + data := make([]byte, 5) + _, err := io.ReadFull(stream, data) + if err != nil { + return nil, err + } + assert.Equal(t, []byte("hello"), data) + return []byte("byeee"), nil + }).Once() + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + RequestHook: hook, + Authenticator: auth, + }) + assert.NoError(t, err) + defer s.Close() + go s.Serve() + + // Create TCP echo server + echoListener, err := net.Listen("tcp", realEchoAddr) + assert.NoError(t, err) + echoServer := &tcpEchoServer{Listener: echoListener} + defer echoServer.Close() + go echoServer.Serve() + + // Create client + c, _, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + }) + assert.NoError(t, err) + defer c.Close() + + // Dial TCP + conn, err := c.TCP(fakeEchoAddr) + assert.NoError(t, err) + defer conn.Close() + + // Send and receive data + sData := []byte("hello world") + _, err = conn.Write(sData) + assert.NoError(t, err) + rData := make([]byte, len(sData)) + _, err = io.ReadFull(conn, rData) + assert.NoError(t, err) + assert.Equal(t, []byte("byeee world"), rData) +} + +func TestClientServerHookUDP(t *testing.T) { + fakeEchoAddr := "hahanope:6666" + realEchoAddr := "127.0.0.1:22333" + + // Create server + udpConn, udpAddr, err := serverConn() + assert.NoError(t, err) + auth := mocks.NewMockAuthenticator(t) + auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") + hook := mocks.NewMockRequestHook(t) + hook.EXPECT().Check(true, fakeEchoAddr).Return(true).Once() + hook.EXPECT().UDP(mock.Anything, mock.Anything).RunAndReturn(func(bytes []byte, s *string) error { + assert.Equal(t, fakeEchoAddr, *s) + assert.Equal(t, []byte("hello world"), bytes) + // Change the address + *s = realEchoAddr + return nil + }).Once() + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + RequestHook: hook, + Authenticator: auth, + }) + assert.NoError(t, err) + defer s.Close() + go s.Serve() + + // Create UDP echo server + echoConn, err := net.ListenPacket("udp", realEchoAddr) + assert.NoError(t, err) + echoServer := &udpEchoServer{Conn: echoConn} + defer echoServer.Close() + go echoServer.Serve() + + // Create client + c, _, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + }) + assert.NoError(t, err) + defer c.Close() + + // Listen UDP + conn, err := c.UDP() + assert.NoError(t, err) + defer conn.Close() + + // Send and receive data + sData := []byte("hello world") + err = conn.Send(sData, fakeEchoAddr) + assert.NoError(t, err) + rData, rAddr, err := conn.Receive() + assert.NoError(t, err) + assert.Equal(t, sData, rData) + // Hook address change is transparent, + // the client should still see the fake echo address it sent packets to + assert.Equal(t, fakeEchoAddr, rAddr) + + // Subsequent packets should also be sent to the real echo server + sData = []byte("never stop fighting") + err = conn.Send(sData, fakeEchoAddr) + assert.NoError(t, err) + rData, rAddr, err = conn.Receive() + assert.NoError(t, err) + assert.Equal(t, sData, rData) + assert.Equal(t, fakeEchoAddr, rAddr) +} diff --git a/v2rayng/hysteria/core/internal/integration_tests/masq_test.go b/v2rayng/hysteria/core/internal/integration_tests/masq_test.go new file mode 100644 index 0000000000..e3dee146ee --- /dev/null +++ b/v2rayng/hysteria/core/internal/integration_tests/masq_test.go @@ -0,0 +1,93 @@ +package integration_tests + +import ( + "context" + "crypto/tls" + "net" + "net/http" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks" + "github.com/apernet/hysteria/core/v2/internal/protocol" + "github.com/apernet/hysteria/core/v2/server" + + "github.com/apernet/quic-go" + "github.com/apernet/quic-go/http3" +) + +// TestServerMasquerade is a test to ensure that the server behaves as a normal +// HTTP/3 server when dealing with an unauthenticated client. This is mainly to +// confirm that the server does not expose itself to active probing. +func TestServerMasquerade(t *testing.T) { + // Create server + udpConn, udpAddr, err := serverConn() + assert.NoError(t, err) + auth := mocks.NewMockAuthenticator(t) + auth.EXPECT().Authenticate(mock.Anything, "", uint64(0)).Return(false, "").Once() + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Authenticator: auth, + }) + assert.NoError(t, err) + defer s.Close() + go s.Serve() + + // QUIC connection & RoundTripper + var conn quic.EarlyConnection + rt := &http3.RoundTripper{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + Dial: func(ctx context.Context, _ string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { + qc, err := quic.DialAddrEarly(ctx, udpAddr.String(), tlsCfg, cfg) + if err != nil { + return nil, err + } + conn = qc + return qc, nil + }, + } + defer rt.Close() // This will close the QUIC connection + + // Send the bogus request + // We expect 404 (from the default handler) + req := &http.Request{ + Method: http.MethodPost, + URL: &url.URL{ + Scheme: "https", + Host: protocol.URLHost, + Path: protocol.URLPath, + }, + Header: make(http.Header), + } + resp, err := rt.RoundTrip(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + for k := range resp.Header { + // Make sure no strange headers are sent by the server + assert.NotContains(t, k, "Hysteria") + } + + buf := make([]byte, 1024) + + // We send a TCP request anyway, see if we get a response + tcpStream, err := conn.OpenStream() + assert.NoError(t, err) + defer tcpStream.Close() + err = protocol.WriteTCPRequest(tcpStream, "www.google.com:443") + assert.NoError(t, err) + + // We should receive nothing + _ = tcpStream.SetReadDeadline(time.Now().Add(2 * time.Second)) + n, err := tcpStream.Read(buf) + assert.Equal(t, 0, n) + nErr, ok := err.(net.Error) + assert.True(t, ok) + assert.True(t, nErr.Timeout()) +} diff --git a/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_Authenticator.go b/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_Authenticator.go new file mode 100644 index 0000000000..b42c7374c7 --- /dev/null +++ b/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_Authenticator.go @@ -0,0 +1,94 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package mocks + +import ( + net "net" + + mock "github.com/stretchr/testify/mock" +) + +// MockAuthenticator is an autogenerated mock type for the Authenticator type +type MockAuthenticator struct { + mock.Mock +} + +type MockAuthenticator_Expecter struct { + mock *mock.Mock +} + +func (_m *MockAuthenticator) EXPECT() *MockAuthenticator_Expecter { + return &MockAuthenticator_Expecter{mock: &_m.Mock} +} + +// Authenticate provides a mock function with given fields: addr, auth, tx +func (_m *MockAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (bool, string) { + ret := _m.Called(addr, auth, tx) + + if len(ret) == 0 { + panic("no return value specified for Authenticate") + } + + var r0 bool + var r1 string + if rf, ok := ret.Get(0).(func(net.Addr, string, uint64) (bool, string)); ok { + return rf(addr, auth, tx) + } + if rf, ok := ret.Get(0).(func(net.Addr, string, uint64) bool); ok { + r0 = rf(addr, auth, tx) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(net.Addr, string, uint64) string); ok { + r1 = rf(addr, auth, tx) + } else { + r1 = ret.Get(1).(string) + } + + return r0, r1 +} + +// MockAuthenticator_Authenticate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Authenticate' +type MockAuthenticator_Authenticate_Call struct { + *mock.Call +} + +// Authenticate is a helper method to define mock.On call +// - addr net.Addr +// - auth string +// - tx uint64 +func (_e *MockAuthenticator_Expecter) Authenticate(addr interface{}, auth interface{}, tx interface{}) *MockAuthenticator_Authenticate_Call { + return &MockAuthenticator_Authenticate_Call{Call: _e.mock.On("Authenticate", addr, auth, tx)} +} + +func (_c *MockAuthenticator_Authenticate_Call) Run(run func(addr net.Addr, auth string, tx uint64)) *MockAuthenticator_Authenticate_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(net.Addr), args[1].(string), args[2].(uint64)) + }) + return _c +} + +func (_c *MockAuthenticator_Authenticate_Call) Return(ok bool, id string) *MockAuthenticator_Authenticate_Call { + _c.Call.Return(ok, id) + return _c +} + +func (_c *MockAuthenticator_Authenticate_Call) RunAndReturn(run func(net.Addr, string, uint64) (bool, string)) *MockAuthenticator_Authenticate_Call { + _c.Call.Return(run) + return _c +} + +// NewMockAuthenticator creates a new instance of MockAuthenticator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockAuthenticator(t interface { + mock.TestingT + Cleanup(func()) +}) *MockAuthenticator { + mock := &MockAuthenticator{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_Conn.go b/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_Conn.go new file mode 100644 index 0000000000..13e363e999 --- /dev/null +++ b/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_Conn.go @@ -0,0 +1,427 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package mocks + +import ( + net "net" + + mock "github.com/stretchr/testify/mock" + + time "time" +) + +// MockConn is an autogenerated mock type for the Conn type +type MockConn struct { + mock.Mock +} + +type MockConn_Expecter struct { + mock *mock.Mock +} + +func (_m *MockConn) EXPECT() *MockConn_Expecter { + return &MockConn_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function with given fields: +func (_m *MockConn) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type MockConn_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *MockConn_Expecter) Close() *MockConn_Close_Call { + return &MockConn_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *MockConn_Close_Call) Run(run func()) *MockConn_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockConn_Close_Call) Return(_a0 error) *MockConn_Close_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConn_Close_Call) RunAndReturn(run func() error) *MockConn_Close_Call { + _c.Call.Return(run) + return _c +} + +// LocalAddr provides a mock function with given fields: +func (_m *MockConn) LocalAddr() net.Addr { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for LocalAddr") + } + + var r0 net.Addr + if rf, ok := ret.Get(0).(func() net.Addr); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(net.Addr) + } + } + + return r0 +} + +// MockConn_LocalAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LocalAddr' +type MockConn_LocalAddr_Call struct { + *mock.Call +} + +// LocalAddr is a helper method to define mock.On call +func (_e *MockConn_Expecter) LocalAddr() *MockConn_LocalAddr_Call { + return &MockConn_LocalAddr_Call{Call: _e.mock.On("LocalAddr")} +} + +func (_c *MockConn_LocalAddr_Call) Run(run func()) *MockConn_LocalAddr_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockConn_LocalAddr_Call) Return(_a0 net.Addr) *MockConn_LocalAddr_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConn_LocalAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_LocalAddr_Call { + _c.Call.Return(run) + return _c +} + +// Read provides a mock function with given fields: b +func (_m *MockConn) Read(b []byte) (int, error) { + ret := _m.Called(b) + + if len(ret) == 0 { + panic("no return value specified for Read") + } + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok { + return rf(b) + } + if rf, ok := ret.Get(0).(func([]byte) int); ok { + r0 = rf(b) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(b) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockConn_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read' +type MockConn_Read_Call struct { + *mock.Call +} + +// Read is a helper method to define mock.On call +// - b []byte +func (_e *MockConn_Expecter) Read(b interface{}) *MockConn_Read_Call { + return &MockConn_Read_Call{Call: _e.mock.On("Read", b)} +} + +func (_c *MockConn_Read_Call) Run(run func(b []byte)) *MockConn_Read_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte)) + }) + return _c +} + +func (_c *MockConn_Read_Call) Return(n int, err error) *MockConn_Read_Call { + _c.Call.Return(n, err) + return _c +} + +func (_c *MockConn_Read_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Read_Call { + _c.Call.Return(run) + return _c +} + +// RemoteAddr provides a mock function with given fields: +func (_m *MockConn) RemoteAddr() net.Addr { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for RemoteAddr") + } + + var r0 net.Addr + if rf, ok := ret.Get(0).(func() net.Addr); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(net.Addr) + } + } + + return r0 +} + +// MockConn_RemoteAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoteAddr' +type MockConn_RemoteAddr_Call struct { + *mock.Call +} + +// RemoteAddr is a helper method to define mock.On call +func (_e *MockConn_Expecter) RemoteAddr() *MockConn_RemoteAddr_Call { + return &MockConn_RemoteAddr_Call{Call: _e.mock.On("RemoteAddr")} +} + +func (_c *MockConn_RemoteAddr_Call) Run(run func()) *MockConn_RemoteAddr_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockConn_RemoteAddr_Call) Return(_a0 net.Addr) *MockConn_RemoteAddr_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConn_RemoteAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_RemoteAddr_Call { + _c.Call.Return(run) + return _c +} + +// SetDeadline provides a mock function with given fields: t +func (_m *MockConn) SetDeadline(t time.Time) error { + ret := _m.Called(t) + + if len(ret) == 0 { + panic("no return value specified for SetDeadline") + } + + var r0 error + if rf, ok := ret.Get(0).(func(time.Time) error); ok { + r0 = rf(t) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockConn_SetDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDeadline' +type MockConn_SetDeadline_Call struct { + *mock.Call +} + +// SetDeadline is a helper method to define mock.On call +// - t time.Time +func (_e *MockConn_Expecter) SetDeadline(t interface{}) *MockConn_SetDeadline_Call { + return &MockConn_SetDeadline_Call{Call: _e.mock.On("SetDeadline", t)} +} + +func (_c *MockConn_SetDeadline_Call) Run(run func(t time.Time)) *MockConn_SetDeadline_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Time)) + }) + return _c +} + +func (_c *MockConn_SetDeadline_Call) Return(_a0 error) *MockConn_SetDeadline_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConn_SetDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetDeadline_Call { + _c.Call.Return(run) + return _c +} + +// SetReadDeadline provides a mock function with given fields: t +func (_m *MockConn) SetReadDeadline(t time.Time) error { + ret := _m.Called(t) + + if len(ret) == 0 { + panic("no return value specified for SetReadDeadline") + } + + var r0 error + if rf, ok := ret.Get(0).(func(time.Time) error); ok { + r0 = rf(t) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockConn_SetReadDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetReadDeadline' +type MockConn_SetReadDeadline_Call struct { + *mock.Call +} + +// SetReadDeadline is a helper method to define mock.On call +// - t time.Time +func (_e *MockConn_Expecter) SetReadDeadline(t interface{}) *MockConn_SetReadDeadline_Call { + return &MockConn_SetReadDeadline_Call{Call: _e.mock.On("SetReadDeadline", t)} +} + +func (_c *MockConn_SetReadDeadline_Call) Run(run func(t time.Time)) *MockConn_SetReadDeadline_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Time)) + }) + return _c +} + +func (_c *MockConn_SetReadDeadline_Call) Return(_a0 error) *MockConn_SetReadDeadline_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConn_SetReadDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetReadDeadline_Call { + _c.Call.Return(run) + return _c +} + +// SetWriteDeadline provides a mock function with given fields: t +func (_m *MockConn) SetWriteDeadline(t time.Time) error { + ret := _m.Called(t) + + if len(ret) == 0 { + panic("no return value specified for SetWriteDeadline") + } + + var r0 error + if rf, ok := ret.Get(0).(func(time.Time) error); ok { + r0 = rf(t) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockConn_SetWriteDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWriteDeadline' +type MockConn_SetWriteDeadline_Call struct { + *mock.Call +} + +// SetWriteDeadline is a helper method to define mock.On call +// - t time.Time +func (_e *MockConn_Expecter) SetWriteDeadline(t interface{}) *MockConn_SetWriteDeadline_Call { + return &MockConn_SetWriteDeadline_Call{Call: _e.mock.On("SetWriteDeadline", t)} +} + +func (_c *MockConn_SetWriteDeadline_Call) Run(run func(t time.Time)) *MockConn_SetWriteDeadline_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Time)) + }) + return _c +} + +func (_c *MockConn_SetWriteDeadline_Call) Return(_a0 error) *MockConn_SetWriteDeadline_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConn_SetWriteDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetWriteDeadline_Call { + _c.Call.Return(run) + return _c +} + +// Write provides a mock function with given fields: b +func (_m *MockConn) Write(b []byte) (int, error) { + ret := _m.Called(b) + + if len(ret) == 0 { + panic("no return value specified for Write") + } + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok { + return rf(b) + } + if rf, ok := ret.Get(0).(func([]byte) int); ok { + r0 = rf(b) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(b) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockConn_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write' +type MockConn_Write_Call struct { + *mock.Call +} + +// Write is a helper method to define mock.On call +// - b []byte +func (_e *MockConn_Expecter) Write(b interface{}) *MockConn_Write_Call { + return &MockConn_Write_Call{Call: _e.mock.On("Write", b)} +} + +func (_c *MockConn_Write_Call) Run(run func(b []byte)) *MockConn_Write_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte)) + }) + return _c +} + +func (_c *MockConn_Write_Call) Return(n int, err error) *MockConn_Write_Call { + _c.Call.Return(n, err) + return _c +} + +func (_c *MockConn_Write_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Write_Call { + _c.Call.Return(run) + return _c +} + +// NewMockConn creates a new instance of MockConn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockConn(t interface { + mock.TestingT + Cleanup(func()) +}) *MockConn { + mock := &MockConn{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_EventLogger.go b/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_EventLogger.go new file mode 100644 index 0000000000..c9f69207f8 --- /dev/null +++ b/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_EventLogger.go @@ -0,0 +1,249 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package mocks + +import ( + net "net" + + mock "github.com/stretchr/testify/mock" +) + +// MockEventLogger is an autogenerated mock type for the EventLogger type +type MockEventLogger struct { + mock.Mock +} + +type MockEventLogger_Expecter struct { + mock *mock.Mock +} + +func (_m *MockEventLogger) EXPECT() *MockEventLogger_Expecter { + return &MockEventLogger_Expecter{mock: &_m.Mock} +} + +// Connect provides a mock function with given fields: addr, id, tx +func (_m *MockEventLogger) Connect(addr net.Addr, id string, tx uint64) { + _m.Called(addr, id, tx) +} + +// MockEventLogger_Connect_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Connect' +type MockEventLogger_Connect_Call struct { + *mock.Call +} + +// Connect is a helper method to define mock.On call +// - addr net.Addr +// - id string +// - tx uint64 +func (_e *MockEventLogger_Expecter) Connect(addr interface{}, id interface{}, tx interface{}) *MockEventLogger_Connect_Call { + return &MockEventLogger_Connect_Call{Call: _e.mock.On("Connect", addr, id, tx)} +} + +func (_c *MockEventLogger_Connect_Call) Run(run func(addr net.Addr, id string, tx uint64)) *MockEventLogger_Connect_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(net.Addr), args[1].(string), args[2].(uint64)) + }) + return _c +} + +func (_c *MockEventLogger_Connect_Call) Return() *MockEventLogger_Connect_Call { + _c.Call.Return() + return _c +} + +func (_c *MockEventLogger_Connect_Call) RunAndReturn(run func(net.Addr, string, uint64)) *MockEventLogger_Connect_Call { + _c.Call.Return(run) + return _c +} + +// Disconnect provides a mock function with given fields: addr, id, err +func (_m *MockEventLogger) Disconnect(addr net.Addr, id string, err error) { + _m.Called(addr, id, err) +} + +// MockEventLogger_Disconnect_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Disconnect' +type MockEventLogger_Disconnect_Call struct { + *mock.Call +} + +// Disconnect is a helper method to define mock.On call +// - addr net.Addr +// - id string +// - err error +func (_e *MockEventLogger_Expecter) Disconnect(addr interface{}, id interface{}, err interface{}) *MockEventLogger_Disconnect_Call { + return &MockEventLogger_Disconnect_Call{Call: _e.mock.On("Disconnect", addr, id, err)} +} + +func (_c *MockEventLogger_Disconnect_Call) Run(run func(addr net.Addr, id string, err error)) *MockEventLogger_Disconnect_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(net.Addr), args[1].(string), args[2].(error)) + }) + return _c +} + +func (_c *MockEventLogger_Disconnect_Call) Return() *MockEventLogger_Disconnect_Call { + _c.Call.Return() + return _c +} + +func (_c *MockEventLogger_Disconnect_Call) RunAndReturn(run func(net.Addr, string, error)) *MockEventLogger_Disconnect_Call { + _c.Call.Return(run) + return _c +} + +// TCPError provides a mock function with given fields: addr, id, reqAddr, err +func (_m *MockEventLogger) TCPError(addr net.Addr, id string, reqAddr string, err error) { + _m.Called(addr, id, reqAddr, err) +} + +// MockEventLogger_TCPError_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCPError' +type MockEventLogger_TCPError_Call struct { + *mock.Call +} + +// TCPError is a helper method to define mock.On call +// - addr net.Addr +// - id string +// - reqAddr string +// - err error +func (_e *MockEventLogger_Expecter) TCPError(addr interface{}, id interface{}, reqAddr interface{}, err interface{}) *MockEventLogger_TCPError_Call { + return &MockEventLogger_TCPError_Call{Call: _e.mock.On("TCPError", addr, id, reqAddr, err)} +} + +func (_c *MockEventLogger_TCPError_Call) Run(run func(addr net.Addr, id string, reqAddr string, err error)) *MockEventLogger_TCPError_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(net.Addr), args[1].(string), args[2].(string), args[3].(error)) + }) + return _c +} + +func (_c *MockEventLogger_TCPError_Call) Return() *MockEventLogger_TCPError_Call { + _c.Call.Return() + return _c +} + +func (_c *MockEventLogger_TCPError_Call) RunAndReturn(run func(net.Addr, string, string, error)) *MockEventLogger_TCPError_Call { + _c.Call.Return(run) + return _c +} + +// TCPRequest provides a mock function with given fields: addr, id, reqAddr +func (_m *MockEventLogger) TCPRequest(addr net.Addr, id string, reqAddr string) { + _m.Called(addr, id, reqAddr) +} + +// MockEventLogger_TCPRequest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCPRequest' +type MockEventLogger_TCPRequest_Call struct { + *mock.Call +} + +// TCPRequest is a helper method to define mock.On call +// - addr net.Addr +// - id string +// - reqAddr string +func (_e *MockEventLogger_Expecter) TCPRequest(addr interface{}, id interface{}, reqAddr interface{}) *MockEventLogger_TCPRequest_Call { + return &MockEventLogger_TCPRequest_Call{Call: _e.mock.On("TCPRequest", addr, id, reqAddr)} +} + +func (_c *MockEventLogger_TCPRequest_Call) Run(run func(addr net.Addr, id string, reqAddr string)) *MockEventLogger_TCPRequest_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(net.Addr), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *MockEventLogger_TCPRequest_Call) Return() *MockEventLogger_TCPRequest_Call { + _c.Call.Return() + return _c +} + +func (_c *MockEventLogger_TCPRequest_Call) RunAndReturn(run func(net.Addr, string, string)) *MockEventLogger_TCPRequest_Call { + _c.Call.Return(run) + return _c +} + +// UDPError provides a mock function with given fields: addr, id, sessionID, err +func (_m *MockEventLogger) UDPError(addr net.Addr, id string, sessionID uint32, err error) { + _m.Called(addr, id, sessionID, err) +} + +// MockEventLogger_UDPError_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDPError' +type MockEventLogger_UDPError_Call struct { + *mock.Call +} + +// UDPError is a helper method to define mock.On call +// - addr net.Addr +// - id string +// - sessionID uint32 +// - err error +func (_e *MockEventLogger_Expecter) UDPError(addr interface{}, id interface{}, sessionID interface{}, err interface{}) *MockEventLogger_UDPError_Call { + return &MockEventLogger_UDPError_Call{Call: _e.mock.On("UDPError", addr, id, sessionID, err)} +} + +func (_c *MockEventLogger_UDPError_Call) Run(run func(addr net.Addr, id string, sessionID uint32, err error)) *MockEventLogger_UDPError_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(net.Addr), args[1].(string), args[2].(uint32), args[3].(error)) + }) + return _c +} + +func (_c *MockEventLogger_UDPError_Call) Return() *MockEventLogger_UDPError_Call { + _c.Call.Return() + return _c +} + +func (_c *MockEventLogger_UDPError_Call) RunAndReturn(run func(net.Addr, string, uint32, error)) *MockEventLogger_UDPError_Call { + _c.Call.Return(run) + return _c +} + +// UDPRequest provides a mock function with given fields: addr, id, sessionID, reqAddr +func (_m *MockEventLogger) UDPRequest(addr net.Addr, id string, sessionID uint32, reqAddr string) { + _m.Called(addr, id, sessionID, reqAddr) +} + +// MockEventLogger_UDPRequest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDPRequest' +type MockEventLogger_UDPRequest_Call struct { + *mock.Call +} + +// UDPRequest is a helper method to define mock.On call +// - addr net.Addr +// - id string +// - sessionID uint32 +// - reqAddr string +func (_e *MockEventLogger_Expecter) UDPRequest(addr interface{}, id interface{}, sessionID interface{}, reqAddr interface{}) *MockEventLogger_UDPRequest_Call { + return &MockEventLogger_UDPRequest_Call{Call: _e.mock.On("UDPRequest", addr, id, sessionID, reqAddr)} +} + +func (_c *MockEventLogger_UDPRequest_Call) Run(run func(addr net.Addr, id string, sessionID uint32, reqAddr string)) *MockEventLogger_UDPRequest_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(net.Addr), args[1].(string), args[2].(uint32), args[3].(string)) + }) + return _c +} + +func (_c *MockEventLogger_UDPRequest_Call) Return() *MockEventLogger_UDPRequest_Call { + _c.Call.Return() + return _c +} + +func (_c *MockEventLogger_UDPRequest_Call) RunAndReturn(run func(net.Addr, string, uint32, string)) *MockEventLogger_UDPRequest_Call { + _c.Call.Return(run) + return _c +} + +// NewMockEventLogger creates a new instance of MockEventLogger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockEventLogger(t interface { + mock.TestingT + Cleanup(func()) +}) *MockEventLogger { + mock := &MockEventLogger{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_Outbound.go b/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_Outbound.go new file mode 100644 index 0000000000..52f71785c6 --- /dev/null +++ b/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_Outbound.go @@ -0,0 +1,154 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package mocks + +import ( + net "net" + + mock "github.com/stretchr/testify/mock" + + server "github.com/apernet/hysteria/core/v2/server" +) + +// MockOutbound is an autogenerated mock type for the Outbound type +type MockOutbound struct { + mock.Mock +} + +type MockOutbound_Expecter struct { + mock *mock.Mock +} + +func (_m *MockOutbound) EXPECT() *MockOutbound_Expecter { + return &MockOutbound_Expecter{mock: &_m.Mock} +} + +// TCP provides a mock function with given fields: reqAddr +func (_m *MockOutbound) TCP(reqAddr string) (net.Conn, error) { + ret := _m.Called(reqAddr) + + if len(ret) == 0 { + panic("no return value specified for TCP") + } + + var r0 net.Conn + var r1 error + if rf, ok := ret.Get(0).(func(string) (net.Conn, error)); ok { + return rf(reqAddr) + } + if rf, ok := ret.Get(0).(func(string) net.Conn); ok { + r0 = rf(reqAddr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(net.Conn) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(reqAddr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockOutbound_TCP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCP' +type MockOutbound_TCP_Call struct { + *mock.Call +} + +// TCP is a helper method to define mock.On call +// - reqAddr string +func (_e *MockOutbound_Expecter) TCP(reqAddr interface{}) *MockOutbound_TCP_Call { + return &MockOutbound_TCP_Call{Call: _e.mock.On("TCP", reqAddr)} +} + +func (_c *MockOutbound_TCP_Call) Run(run func(reqAddr string)) *MockOutbound_TCP_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockOutbound_TCP_Call) Return(_a0 net.Conn, _a1 error) *MockOutbound_TCP_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockOutbound_TCP_Call) RunAndReturn(run func(string) (net.Conn, error)) *MockOutbound_TCP_Call { + _c.Call.Return(run) + return _c +} + +// UDP provides a mock function with given fields: reqAddr +func (_m *MockOutbound) UDP(reqAddr string) (server.UDPConn, error) { + ret := _m.Called(reqAddr) + + if len(ret) == 0 { + panic("no return value specified for UDP") + } + + var r0 server.UDPConn + var r1 error + if rf, ok := ret.Get(0).(func(string) (server.UDPConn, error)); ok { + return rf(reqAddr) + } + if rf, ok := ret.Get(0).(func(string) server.UDPConn); ok { + r0 = rf(reqAddr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(server.UDPConn) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(reqAddr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockOutbound_UDP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDP' +type MockOutbound_UDP_Call struct { + *mock.Call +} + +// UDP is a helper method to define mock.On call +// - reqAddr string +func (_e *MockOutbound_Expecter) UDP(reqAddr interface{}) *MockOutbound_UDP_Call { + return &MockOutbound_UDP_Call{Call: _e.mock.On("UDP", reqAddr)} +} + +func (_c *MockOutbound_UDP_Call) Run(run func(reqAddr string)) *MockOutbound_UDP_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockOutbound_UDP_Call) Return(_a0 server.UDPConn, _a1 error) *MockOutbound_UDP_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockOutbound_UDP_Call) RunAndReturn(run func(string) (server.UDPConn, error)) *MockOutbound_UDP_Call { + _c.Call.Return(run) + return _c +} + +// NewMockOutbound creates a new instance of MockOutbound. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockOutbound(t interface { + mock.TestingT + Cleanup(func()) +}) *MockOutbound { + mock := &MockOutbound{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_RequestHook.go b/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_RequestHook.go new file mode 100644 index 0000000000..5418eaf72d --- /dev/null +++ b/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_RequestHook.go @@ -0,0 +1,188 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package mocks + +import ( + quic "github.com/apernet/quic-go" + mock "github.com/stretchr/testify/mock" +) + +// MockRequestHook is an autogenerated mock type for the RequestHook type +type MockRequestHook struct { + mock.Mock +} + +type MockRequestHook_Expecter struct { + mock *mock.Mock +} + +func (_m *MockRequestHook) EXPECT() *MockRequestHook_Expecter { + return &MockRequestHook_Expecter{mock: &_m.Mock} +} + +// Check provides a mock function with given fields: isUDP, reqAddr +func (_m *MockRequestHook) Check(isUDP bool, reqAddr string) bool { + ret := _m.Called(isUDP, reqAddr) + + if len(ret) == 0 { + panic("no return value specified for Check") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(bool, string) bool); ok { + r0 = rf(isUDP, reqAddr) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// MockRequestHook_Check_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Check' +type MockRequestHook_Check_Call struct { + *mock.Call +} + +// Check is a helper method to define mock.On call +// - isUDP bool +// - reqAddr string +func (_e *MockRequestHook_Expecter) Check(isUDP interface{}, reqAddr interface{}) *MockRequestHook_Check_Call { + return &MockRequestHook_Check_Call{Call: _e.mock.On("Check", isUDP, reqAddr)} +} + +func (_c *MockRequestHook_Check_Call) Run(run func(isUDP bool, reqAddr string)) *MockRequestHook_Check_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool), args[1].(string)) + }) + return _c +} + +func (_c *MockRequestHook_Check_Call) Return(_a0 bool) *MockRequestHook_Check_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockRequestHook_Check_Call) RunAndReturn(run func(bool, string) bool) *MockRequestHook_Check_Call { + _c.Call.Return(run) + return _c +} + +// TCP provides a mock function with given fields: stream, reqAddr +func (_m *MockRequestHook) TCP(stream quic.Stream, reqAddr *string) ([]byte, error) { + ret := _m.Called(stream, reqAddr) + + if len(ret) == 0 { + panic("no return value specified for TCP") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(quic.Stream, *string) ([]byte, error)); ok { + return rf(stream, reqAddr) + } + if rf, ok := ret.Get(0).(func(quic.Stream, *string) []byte); ok { + r0 = rf(stream, reqAddr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(quic.Stream, *string) error); ok { + r1 = rf(stream, reqAddr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRequestHook_TCP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCP' +type MockRequestHook_TCP_Call struct { + *mock.Call +} + +// TCP is a helper method to define mock.On call +// - stream quic.Stream +// - reqAddr *string +func (_e *MockRequestHook_Expecter) TCP(stream interface{}, reqAddr interface{}) *MockRequestHook_TCP_Call { + return &MockRequestHook_TCP_Call{Call: _e.mock.On("TCP", stream, reqAddr)} +} + +func (_c *MockRequestHook_TCP_Call) Run(run func(stream quic.Stream, reqAddr *string)) *MockRequestHook_TCP_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(quic.Stream), args[1].(*string)) + }) + return _c +} + +func (_c *MockRequestHook_TCP_Call) Return(_a0 []byte, _a1 error) *MockRequestHook_TCP_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockRequestHook_TCP_Call) RunAndReturn(run func(quic.Stream, *string) ([]byte, error)) *MockRequestHook_TCP_Call { + _c.Call.Return(run) + return _c +} + +// UDP provides a mock function with given fields: data, reqAddr +func (_m *MockRequestHook) UDP(data []byte, reqAddr *string) error { + ret := _m.Called(data, reqAddr) + + if len(ret) == 0 { + panic("no return value specified for UDP") + } + + var r0 error + if rf, ok := ret.Get(0).(func([]byte, *string) error); ok { + r0 = rf(data, reqAddr) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockRequestHook_UDP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDP' +type MockRequestHook_UDP_Call struct { + *mock.Call +} + +// UDP is a helper method to define mock.On call +// - data []byte +// - reqAddr *string +func (_e *MockRequestHook_Expecter) UDP(data interface{}, reqAddr interface{}) *MockRequestHook_UDP_Call { + return &MockRequestHook_UDP_Call{Call: _e.mock.On("UDP", data, reqAddr)} +} + +func (_c *MockRequestHook_UDP_Call) Run(run func(data []byte, reqAddr *string)) *MockRequestHook_UDP_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte), args[1].(*string)) + }) + return _c +} + +func (_c *MockRequestHook_UDP_Call) Return(_a0 error) *MockRequestHook_UDP_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockRequestHook_UDP_Call) RunAndReturn(run func([]byte, *string) error) *MockRequestHook_UDP_Call { + _c.Call.Return(run) + return _c +} + +// NewMockRequestHook creates a new instance of MockRequestHook. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockRequestHook(t interface { + mock.TestingT + Cleanup(func()) +}) *MockRequestHook { + mock := &MockRequestHook{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_TrafficLogger.go b/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_TrafficLogger.go new file mode 100644 index 0000000000..1ed977efd0 --- /dev/null +++ b/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_TrafficLogger.go @@ -0,0 +1,186 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package mocks + +import ( + quic "github.com/apernet/quic-go" + mock "github.com/stretchr/testify/mock" + + server "github.com/apernet/hysteria/core/v2/server" +) + +// MockTrafficLogger is an autogenerated mock type for the TrafficLogger type +type MockTrafficLogger struct { + mock.Mock +} + +type MockTrafficLogger_Expecter struct { + mock *mock.Mock +} + +func (_m *MockTrafficLogger) EXPECT() *MockTrafficLogger_Expecter { + return &MockTrafficLogger_Expecter{mock: &_m.Mock} +} + +// LogOnlineState provides a mock function with given fields: id, online +func (_m *MockTrafficLogger) LogOnlineState(id string, online bool) { + _m.Called(id, online) +} + +// MockTrafficLogger_LogOnlineState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LogOnlineState' +type MockTrafficLogger_LogOnlineState_Call struct { + *mock.Call +} + +// LogOnlineState is a helper method to define mock.On call +// - id string +// - online bool +func (_e *MockTrafficLogger_Expecter) LogOnlineState(id interface{}, online interface{}) *MockTrafficLogger_LogOnlineState_Call { + return &MockTrafficLogger_LogOnlineState_Call{Call: _e.mock.On("LogOnlineState", id, online)} +} + +func (_c *MockTrafficLogger_LogOnlineState_Call) Run(run func(id string, online bool)) *MockTrafficLogger_LogOnlineState_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(bool)) + }) + return _c +} + +func (_c *MockTrafficLogger_LogOnlineState_Call) Return() *MockTrafficLogger_LogOnlineState_Call { + _c.Call.Return() + return _c +} + +func (_c *MockTrafficLogger_LogOnlineState_Call) RunAndReturn(run func(string, bool)) *MockTrafficLogger_LogOnlineState_Call { + _c.Call.Return(run) + return _c +} + +// LogTraffic provides a mock function with given fields: id, tx, rx +func (_m *MockTrafficLogger) LogTraffic(id string, tx uint64, rx uint64) bool { + ret := _m.Called(id, tx, rx) + + if len(ret) == 0 { + panic("no return value specified for LogTraffic") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(string, uint64, uint64) bool); ok { + r0 = rf(id, tx, rx) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// MockTrafficLogger_LogTraffic_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LogTraffic' +type MockTrafficLogger_LogTraffic_Call struct { + *mock.Call +} + +// LogTraffic is a helper method to define mock.On call +// - id string +// - tx uint64 +// - rx uint64 +func (_e *MockTrafficLogger_Expecter) LogTraffic(id interface{}, tx interface{}, rx interface{}) *MockTrafficLogger_LogTraffic_Call { + return &MockTrafficLogger_LogTraffic_Call{Call: _e.mock.On("LogTraffic", id, tx, rx)} +} + +func (_c *MockTrafficLogger_LogTraffic_Call) Run(run func(id string, tx uint64, rx uint64)) *MockTrafficLogger_LogTraffic_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(uint64), args[2].(uint64)) + }) + return _c +} + +func (_c *MockTrafficLogger_LogTraffic_Call) Return(ok bool) *MockTrafficLogger_LogTraffic_Call { + _c.Call.Return(ok) + return _c +} + +func (_c *MockTrafficLogger_LogTraffic_Call) RunAndReturn(run func(string, uint64, uint64) bool) *MockTrafficLogger_LogTraffic_Call { + _c.Call.Return(run) + return _c +} + +// TraceStream provides a mock function with given fields: stream, stats +func (_m *MockTrafficLogger) TraceStream(stream quic.Stream, stats *server.StreamStats) { + _m.Called(stream, stats) +} + +// MockTrafficLogger_TraceStream_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TraceStream' +type MockTrafficLogger_TraceStream_Call struct { + *mock.Call +} + +// TraceStream is a helper method to define mock.On call +// - stream quic.Stream +// - stats *server.StreamStats +func (_e *MockTrafficLogger_Expecter) TraceStream(stream interface{}, stats interface{}) *MockTrafficLogger_TraceStream_Call { + return &MockTrafficLogger_TraceStream_Call{Call: _e.mock.On("TraceStream", stream, stats)} +} + +func (_c *MockTrafficLogger_TraceStream_Call) Run(run func(stream quic.Stream, stats *server.StreamStats)) *MockTrafficLogger_TraceStream_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(quic.Stream), args[1].(*server.StreamStats)) + }) + return _c +} + +func (_c *MockTrafficLogger_TraceStream_Call) Return() *MockTrafficLogger_TraceStream_Call { + _c.Call.Return() + return _c +} + +func (_c *MockTrafficLogger_TraceStream_Call) RunAndReturn(run func(quic.Stream, *server.StreamStats)) *MockTrafficLogger_TraceStream_Call { + _c.Call.Return(run) + return _c +} + +// UntraceStream provides a mock function with given fields: stream +func (_m *MockTrafficLogger) UntraceStream(stream quic.Stream) { + _m.Called(stream) +} + +// MockTrafficLogger_UntraceStream_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UntraceStream' +type MockTrafficLogger_UntraceStream_Call struct { + *mock.Call +} + +// UntraceStream is a helper method to define mock.On call +// - stream quic.Stream +func (_e *MockTrafficLogger_Expecter) UntraceStream(stream interface{}) *MockTrafficLogger_UntraceStream_Call { + return &MockTrafficLogger_UntraceStream_Call{Call: _e.mock.On("UntraceStream", stream)} +} + +func (_c *MockTrafficLogger_UntraceStream_Call) Run(run func(stream quic.Stream)) *MockTrafficLogger_UntraceStream_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(quic.Stream)) + }) + return _c +} + +func (_c *MockTrafficLogger_UntraceStream_Call) Return() *MockTrafficLogger_UntraceStream_Call { + _c.Call.Return() + return _c +} + +func (_c *MockTrafficLogger_UntraceStream_Call) RunAndReturn(run func(quic.Stream)) *MockTrafficLogger_UntraceStream_Call { + _c.Call.Return(run) + return _c +} + +// NewMockTrafficLogger creates a new instance of MockTrafficLogger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockTrafficLogger(t interface { + mock.TestingT + Cleanup(func()) +}) *MockTrafficLogger { + mock := &MockTrafficLogger{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_UDPConn.go b/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_UDPConn.go new file mode 100644 index 0000000000..b88387086d --- /dev/null +++ b/v2rayng/hysteria/core/internal/integration_tests/mocks/mock_UDPConn.go @@ -0,0 +1,197 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// MockUDPConn is an autogenerated mock type for the UDPConn type +type MockUDPConn struct { + mock.Mock +} + +type MockUDPConn_Expecter struct { + mock *mock.Mock +} + +func (_m *MockUDPConn) EXPECT() *MockUDPConn_Expecter { + return &MockUDPConn_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function with given fields: +func (_m *MockUDPConn) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockUDPConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type MockUDPConn_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *MockUDPConn_Expecter) Close() *MockUDPConn_Close_Call { + return &MockUDPConn_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *MockUDPConn_Close_Call) Run(run func()) *MockUDPConn_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockUDPConn_Close_Call) Return(_a0 error) *MockUDPConn_Close_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockUDPConn_Close_Call) RunAndReturn(run func() error) *MockUDPConn_Close_Call { + _c.Call.Return(run) + return _c +} + +// ReadFrom provides a mock function with given fields: b +func (_m *MockUDPConn) ReadFrom(b []byte) (int, string, error) { + ret := _m.Called(b) + + if len(ret) == 0 { + panic("no return value specified for ReadFrom") + } + + var r0 int + var r1 string + var r2 error + if rf, ok := ret.Get(0).(func([]byte) (int, string, error)); ok { + return rf(b) + } + if rf, ok := ret.Get(0).(func([]byte) int); ok { + r0 = rf(b) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func([]byte) string); ok { + r1 = rf(b) + } else { + r1 = ret.Get(1).(string) + } + + if rf, ok := ret.Get(2).(func([]byte) error); ok { + r2 = rf(b) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// MockUDPConn_ReadFrom_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadFrom' +type MockUDPConn_ReadFrom_Call struct { + *mock.Call +} + +// ReadFrom is a helper method to define mock.On call +// - b []byte +func (_e *MockUDPConn_Expecter) ReadFrom(b interface{}) *MockUDPConn_ReadFrom_Call { + return &MockUDPConn_ReadFrom_Call{Call: _e.mock.On("ReadFrom", b)} +} + +func (_c *MockUDPConn_ReadFrom_Call) Run(run func(b []byte)) *MockUDPConn_ReadFrom_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte)) + }) + return _c +} + +func (_c *MockUDPConn_ReadFrom_Call) Return(_a0 int, _a1 string, _a2 error) *MockUDPConn_ReadFrom_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *MockUDPConn_ReadFrom_Call) RunAndReturn(run func([]byte) (int, string, error)) *MockUDPConn_ReadFrom_Call { + _c.Call.Return(run) + return _c +} + +// WriteTo provides a mock function with given fields: b, addr +func (_m *MockUDPConn) WriteTo(b []byte, addr string) (int, error) { + ret := _m.Called(b, addr) + + if len(ret) == 0 { + panic("no return value specified for WriteTo") + } + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func([]byte, string) (int, error)); ok { + return rf(b, addr) + } + if rf, ok := ret.Get(0).(func([]byte, string) int); ok { + r0 = rf(b, addr) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func([]byte, string) error); ok { + r1 = rf(b, addr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockUDPConn_WriteTo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteTo' +type MockUDPConn_WriteTo_Call struct { + *mock.Call +} + +// WriteTo is a helper method to define mock.On call +// - b []byte +// - addr string +func (_e *MockUDPConn_Expecter) WriteTo(b interface{}, addr interface{}) *MockUDPConn_WriteTo_Call { + return &MockUDPConn_WriteTo_Call{Call: _e.mock.On("WriteTo", b, addr)} +} + +func (_c *MockUDPConn_WriteTo_Call) Run(run func(b []byte, addr string)) *MockUDPConn_WriteTo_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte), args[1].(string)) + }) + return _c +} + +func (_c *MockUDPConn_WriteTo_Call) Return(_a0 int, _a1 error) *MockUDPConn_WriteTo_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockUDPConn_WriteTo_Call) RunAndReturn(run func([]byte, string) (int, error)) *MockUDPConn_WriteTo_Call { + _c.Call.Return(run) + return _c +} + +// NewMockUDPConn creates a new instance of MockUDPConn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockUDPConn(t interface { + mock.TestingT + Cleanup(func()) +}) *MockUDPConn { + mock := &MockUDPConn{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/v2rayng/hysteria/core/internal/integration_tests/smoke_test.go b/v2rayng/hysteria/core/internal/integration_tests/smoke_test.go new file mode 100644 index 0000000000..5288b6107b --- /dev/null +++ b/v2rayng/hysteria/core/internal/integration_tests/smoke_test.go @@ -0,0 +1,280 @@ +package integration_tests + +import ( + "io" + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/apernet/hysteria/core/v2/client" + coreErrs "github.com/apernet/hysteria/core/v2/errors" + "github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks" + "github.com/apernet/hysteria/core/v2/server" +) + +// Smoke tests that act as a sanity check for client & server to ensure they can talk to each other correctly. + +// TestClientNoServer tests how the client handles a server address it cannot connect to. +// NewClient should return a ConnectError. +func TestClientNoServer(t *testing.T) { + c, _, err := client.NewClient(&client.Config{ + ServerAddr: &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 55666}, + }) + assert.Nil(t, c) + _, ok := err.(coreErrs.ConnectError) + assert.True(t, ok) +} + +// TestClientServerBadAuth tests two things: +// - The server uses Authenticator when a client connects. +// - How the client handles failed authentication. +func TestClientServerBadAuth(t *testing.T) { + // Create server + udpConn, udpAddr, err := serverConn() + assert.NoError(t, err) + auth := mocks.NewMockAuthenticator(t) + auth.EXPECT().Authenticate(mock.Anything, "badpassword", uint64(0)).Return(false, "").Once() + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Authenticator: auth, + }) + assert.NoError(t, err) + defer s.Close() + go s.Serve() + + // Create client + c, _, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + Auth: "badpassword", + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + }) + assert.Nil(t, c) + _, ok := err.(coreErrs.AuthError) + assert.True(t, ok) +} + +// TestClientServerUDPDisabled tests how the client handles a server that does not support UDP. +// UDP should return a DialError. +func TestClientServerUDPDisabled(t *testing.T) { + // Create server + udpConn, udpAddr, err := serverConn() + assert.NoError(t, err) + auth := mocks.NewMockAuthenticator(t) + auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + DisableUDP: true, + Authenticator: auth, + }) + assert.NoError(t, err) + defer s.Close() + go s.Serve() + + // Create client + c, _, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + }) + assert.NoError(t, err) + defer c.Close() + + conn, err := c.UDP() + assert.Nil(t, conn) + _, ok := err.(coreErrs.DialError) + assert.True(t, ok) +} + +// TestClientServerTCPEcho tests TCP forwarding using a TCP echo server. +func TestClientServerTCPEcho(t *testing.T) { + // Create server + udpConn, udpAddr, err := serverConn() + assert.NoError(t, err) + auth := mocks.NewMockAuthenticator(t) + auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Authenticator: auth, + }) + assert.NoError(t, err) + defer s.Close() + go s.Serve() + + // Create TCP echo server + echoAddr := "127.0.0.1:22333" + echoListener, err := net.Listen("tcp", echoAddr) + assert.NoError(t, err) + echoServer := &tcpEchoServer{Listener: echoListener} + defer echoServer.Close() + go echoServer.Serve() + + // Create client + c, _, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + }) + assert.NoError(t, err) + defer c.Close() + + // Dial TCP + conn, err := c.TCP(echoAddr) + assert.NoError(t, err) + defer conn.Close() + + // Send and receive data + sData := []byte("hello world") + _, err = conn.Write(sData) + assert.NoError(t, err) + rData := make([]byte, len(sData)) + _, err = io.ReadFull(conn, rData) + assert.NoError(t, err) + assert.Equal(t, sData, rData) +} + +// TestClientServerUDPEcho tests UDP forwarding using a UDP echo server. +func TestClientServerUDPEcho(t *testing.T) { + // Create server + udpConn, udpAddr, err := serverConn() + assert.NoError(t, err) + auth := mocks.NewMockAuthenticator(t) + auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Authenticator: auth, + }) + assert.NoError(t, err) + defer s.Close() + go s.Serve() + + // Create UDP echo server + echoAddr := "127.0.0.1:22333" + echoConn, err := net.ListenPacket("udp", echoAddr) + assert.NoError(t, err) + echoServer := &udpEchoServer{Conn: echoConn} + defer echoServer.Close() + go echoServer.Serve() + + // Create client + c, _, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + }) + assert.NoError(t, err) + defer c.Close() + + // Listen UDP + conn, err := c.UDP() + assert.NoError(t, err) + defer conn.Close() + + // Send and receive data + sData := []byte("hello world") + err = conn.Send(sData, echoAddr) + assert.NoError(t, err) + rData, rAddr, err := conn.Receive() + assert.NoError(t, err) + assert.Equal(t, sData, rData) + assert.Equal(t, echoAddr, rAddr) +} + +// TestClientServerHandshakeInfo tests that the client returns the correct handshake info. +func TestClientServerHandshakeInfo(t *testing.T) { + // Create server 1, UDP enabled, unlimited bandwidth + udpConn, udpAddr, err := serverConn() + assert.NoError(t, err) + auth := mocks.NewMockAuthenticator(t) + auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Authenticator: auth, + }) + assert.NoError(t, err) + go s.Serve() + + // Create client 1, with specified tx bandwidth + c, info, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + BandwidthConfig: client.BandwidthConfig{ + MaxTx: 123456, + }, + }) + assert.NoError(t, err) + assert.Equal(t, &client.HandshakeInfo{ + UDPEnabled: true, + Tx: 123456, + }, info) + + // Close server 1 and client 1 + _ = s.Close() + _ = c.Close() + + // Create server 2, UDP disabled, limited rx bandwidth + udpConn, udpAddr, err = serverConn() + assert.NoError(t, err) + s, err = server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + BandwidthConfig: server.BandwidthConfig{ + MaxRx: 100000, + }, + DisableUDP: true, + Authenticator: auth, + }) + assert.NoError(t, err) + go s.Serve() + + // Create client 2, with specified tx bandwidth + c, info, err = client.NewClient(&client.Config{ + ServerAddr: udpAddr, + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + BandwidthConfig: client.BandwidthConfig{ + MaxTx: 123456, + }, + }) + assert.NoError(t, err) + assert.Equal(t, &client.HandshakeInfo{ + UDPEnabled: false, + Tx: 100000, + }, info) + + // Close server 2 and client 2 + _ = s.Close() + _ = c.Close() + + // Create server 3, UDP enabled, ignore client bandwidth + udpConn, udpAddr, err = serverConn() + assert.NoError(t, err) + s, err = server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + IgnoreClientBandwidth: true, + Authenticator: auth, + }) + assert.NoError(t, err) + go s.Serve() + + // Create client 3, with specified tx bandwidth + c, info, err = client.NewClient(&client.Config{ + ServerAddr: udpAddr, + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + BandwidthConfig: client.BandwidthConfig{ + MaxTx: 123456, + }, + }) + assert.NoError(t, err) + assert.Equal(t, &client.HandshakeInfo{ + UDPEnabled: true, + Tx: 0, + }, info) + + // Close server 3 and client 3 + _ = s.Close() + _ = c.Close() +} diff --git a/v2rayng/hysteria/core/internal/integration_tests/stress_test.go b/v2rayng/hysteria/core/internal/integration_tests/stress_test.go new file mode 100644 index 0000000000..cf44ad6413 --- /dev/null +++ b/v2rayng/hysteria/core/internal/integration_tests/stress_test.go @@ -0,0 +1,252 @@ +package integration_tests + +import ( + "context" + "crypto/rand" + "fmt" + "io" + "net" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "golang.org/x/time/rate" + + "github.com/apernet/hysteria/core/v2/client" + "github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks" + "github.com/apernet/hysteria/core/v2/server" +) + +type tcpStressor struct { + DialFunc func() (net.Conn, error) + Size int + Parallel int + Iterations int +} + +func (s *tcpStressor) Run(t *testing.T) { + // Make some random data + sData := make([]byte, s.Size) + _, err := rand.Read(sData) + assert.NoError(t, err) + + // Run iterations + for i := 0; i < s.Iterations; i++ { + var wg sync.WaitGroup + errChan := make(chan error, s.Parallel) + for j := 0; j < s.Parallel; j++ { + wg.Add(1) + go func() { + defer wg.Done() + + conn, err := s.DialFunc() + if err != nil { + errChan <- err + return + } + defer conn.Close() + go conn.Write(sData) + + rData := make([]byte, len(sData)) + _, err = io.ReadFull(conn, rData) + if err != nil { + errChan <- err + return + } + }() + } + wg.Wait() + + assert.Empty(t, errChan) + } +} + +type udpStressor struct { + ListenFunc func() (client.HyUDPConn, error) + ServerAddr string + Size int + Count int + Parallel int + Iterations int +} + +func (s *udpStressor) Run(t *testing.T) { + // Make some random data + sData := make([]byte, s.Size) + _, err := rand.Read(sData) + assert.NoError(t, err) + + // Due to UDP's unreliability, we need to limit the rate of sending + // to reduce packet loss. This is hardcoded to 1 MiB/s for now. + limiter := rate.NewLimiter(1048576, 1048576) + + // Run iterations + for i := 0; i < s.Iterations; i++ { + var wg sync.WaitGroup + errChan := make(chan error, s.Parallel) + for j := 0; j < s.Parallel; j++ { + wg.Add(1) + go func() { + defer wg.Done() + + conn, err := s.ListenFunc() + if err != nil { + errChan <- err + return + } + defer conn.Close() + go func() { + // Sending routine + for i := 0; i < s.Count; i++ { + _ = limiter.WaitN(context.Background(), len(sData)) + _ = conn.Send(sData, s.ServerAddr) + } + }() + + minCount := s.Count * 8 / 10 // Tolerate 20% packet loss + for i := 0; i < minCount; i++ { + rData, _, err := conn.Receive() + if err != nil { + errChan <- err + return + } + if len(rData) != len(sData) { + errChan <- fmt.Errorf("incomplete data received: %d/%d bytes", len(rData), len(sData)) + return + } + } + }() + } + wg.Wait() + + assert.Empty(t, errChan) + } +} + +func TestClientServerTCPStress(t *testing.T) { + // Create server + udpConn, udpAddr, err := serverConn() + assert.NoError(t, err) + auth := mocks.NewMockAuthenticator(t) + auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Authenticator: auth, + }) + assert.NoError(t, err) + defer s.Close() + go s.Serve() + + // Create TCP echo server + echoAddr := "127.0.0.1:22333" + echoListener, err := net.Listen("tcp", echoAddr) + assert.NoError(t, err) + echoServer := &tcpEchoServer{Listener: echoListener} + defer echoServer.Close() + go echoServer.Serve() + + // Create client + c, _, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + }) + assert.NoError(t, err) + defer c.Close() + + dialFunc := func() (net.Conn, error) { + return c.TCP(echoAddr) + } + + t.Run("Single 500m", (&tcpStressor{DialFunc: dialFunc, Size: 524288000, Parallel: 1, Iterations: 1}).Run) + + t.Run("Sequential 1000x1m", (&tcpStressor{DialFunc: dialFunc, Size: 1048576, Parallel: 1, Iterations: 1000}).Run) + t.Run("Sequential 10000x100k", (&tcpStressor{DialFunc: dialFunc, Size: 102400, Parallel: 1, Iterations: 10000}).Run) + + t.Run("Parallel 100x10m", (&tcpStressor{DialFunc: dialFunc, Size: 10485760, Parallel: 100, Iterations: 1}).Run) + t.Run("Parallel 1000x1m", (&tcpStressor{DialFunc: dialFunc, Size: 1048576, Parallel: 1000, Iterations: 1}).Run) +} + +func TestClientServerUDPStress(t *testing.T) { + // Create server + udpConn, udpAddr, err := serverConn() + assert.NoError(t, err) + auth := mocks.NewMockAuthenticator(t) + auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Authenticator: auth, + }) + assert.NoError(t, err) + defer s.Close() + go s.Serve() + + // Create UDP echo server + echoAddr := "127.0.0.1:22333" + echoConn, err := net.ListenPacket("udp", echoAddr) + assert.NoError(t, err) + echoServer := &udpEchoServer{Conn: echoConn} + defer echoServer.Close() + go echoServer.Serve() + + // Create client + c, _, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + }) + assert.NoError(t, err) + defer c.Close() + + t.Run("Single 1000x100b", (&udpStressor{ + ListenFunc: c.UDP, + ServerAddr: echoAddr, + Size: 100, + Count: 1000, + Parallel: 1, + Iterations: 1, + }).Run) + t.Run("Single 1000x3k", (&udpStressor{ + ListenFunc: c.UDP, + ServerAddr: echoAddr, + Size: 3000, + Count: 1000, + Parallel: 1, + Iterations: 1, + }).Run) + + t.Run("5 Sequential 1000x100b", (&udpStressor{ + ListenFunc: c.UDP, + ServerAddr: echoAddr, + Size: 100, + Count: 1000, + Parallel: 1, + Iterations: 5, + }).Run) + t.Run("5 Sequential 200x3k", (&udpStressor{ + ListenFunc: c.UDP, + ServerAddr: echoAddr, + Size: 3000, + Count: 200, + Parallel: 1, + Iterations: 5, + }).Run) + + t.Run("2 Sequential 5 Parallel 1000x100b", (&udpStressor{ + ListenFunc: c.UDP, + ServerAddr: echoAddr, + Size: 100, + Count: 1000, + Parallel: 5, + Iterations: 2, + }).Run) + t.Run("2 Sequential 5 Parallel 200x3k", (&udpStressor{ + ListenFunc: c.UDP, + ServerAddr: echoAddr, + Size: 3000, + Count: 200, + Parallel: 5, + Iterations: 2, + }).Run) +} diff --git a/v2rayng/hysteria/core/internal/integration_tests/test.crt b/v2rayng/hysteria/core/internal/integration_tests/test.crt new file mode 100644 index 0000000000..ecb00ed17e --- /dev/null +++ b/v2rayng/hysteria/core/internal/integration_tests/test.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDwTCCAqmgAwIBAgIUMeefneiCXWS2ovxNN+fJcdrOIfAwDQYJKoZIhvcNAQEL +BQAwcDELMAkGA1UEBhMCVFcxEzARBgNVBAgMClNvbWUtU3RhdGUxGTAXBgNVBAoM +EFJhbmRvbSBTdHVmZiBMTEMxEjAQBgNVBAMMCWxvY2FsaG9zdDEdMBsGCSqGSIb3 +DQEJARYOcG9vcGVyQHNoaXQuY2MwHhcNMjMwNDI3MDAyMDQ1WhcNMzMwNDI0MDAy +MDQ1WjBwMQswCQYDVQQGEwJUVzETMBEGA1UECAwKU29tZS1TdGF0ZTEZMBcGA1UE +CgwQUmFuZG9tIFN0dWZmIExMQzESMBAGA1UEAwwJbG9jYWxob3N0MR0wGwYJKoZI +hvcNAQkBFg5wb29wZXJAc2hpdC5jYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAOU9/4AT/6fDKyEyZMMLFzUEVC8ZDJHoKZ+3g65ZFQLxRKqlEdhvOwq4 +ZsxYF0sceUPDAsdrT+km0l1jAvq6u82n6xQQ60HpKe6hOvDX7KS0dPcKa+nfEa0W +DKamBB+TzxB2dBfBNS1oUU74nBb7ttpJiKnOpRJ0/J+CwslvhJzq04AUXC/W1CtW +CbZBg1JjY0fCN+Oy1WjEqMtRSB6k5Ipk40a8NcsqReBOMZChR8elruZ09sIlA6tf +jICOKToDVBmkjJ8m/GnxfV8MeLoK83M2VA73njsS6q9qe9KDVgIVQmifwi6JUb7N +o0A6f2Z47AWJmvq4goHJtnQ3fyoeIsMCAwEAAaNTMFEwHQYDVR0OBBYEFPrBsm6v +M29fKA3is22tK8yHYQaDMB8GA1UdIwQYMBaAFPrBsm6vM29fKA3is22tK8yHYQaD +MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJvOwj0Tf8l9AWvf +1ZLyW0K3m5oJAoUayjlLP9q7KHgJHWd4QXxg4ApUDo523m4Own3FwtN06KCMqlxc +luDJi27ghRzZ8bpB9fUujikC1rs1oWYRz/K+JSO1VItan+azm9AQRj+nNepjUiT4 +FjvRif+inC4392tcKuwrqiUFmLIggtFZdsLeKUL+hRGCRjY4BZw0d1sjjPtyVNUD +UMVO8pxlCV0NU4Nmt3vulD4YshAXM+Y8yX/vPRnaNGoRrbRgCg2VORRGaZVjQMHD +OLMvqM7pFKnVg0uiSbQ3xbQJ8WeX620zKI0So2+kZt9HoI+46gd7BdNfl7mmd6K7 +ydYKuI8= +-----END CERTIFICATE----- diff --git a/v2rayng/hysteria/core/internal/integration_tests/test.key b/v2rayng/hysteria/core/internal/integration_tests/test.key new file mode 100644 index 0000000000..d471f50de5 --- /dev/null +++ b/v2rayng/hysteria/core/internal/integration_tests/test.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA5T3/gBP/p8MrITJkwwsXNQRULxkMkegpn7eDrlkVAvFEqqUR +2G87CrhmzFgXSxx5Q8MCx2tP6SbSXWMC+rq7zafrFBDrQekp7qE68NfspLR09wpr +6d8RrRYMpqYEH5PPEHZ0F8E1LWhRTvicFvu22kmIqc6lEnT8n4LCyW+EnOrTgBRc +L9bUK1YJtkGDUmNjR8I347LVaMSoy1FIHqTkimTjRrw1yypF4E4xkKFHx6Wu5nT2 +wiUDq1+MgI4pOgNUGaSMnyb8afF9Xwx4ugrzczZUDveeOxLqr2p70oNWAhVCaJ/C +LolRvs2jQDp/ZnjsBYma+riCgcm2dDd/Kh4iwwIDAQABAoIBABjiU/vJL/U8AFCI +MdviNlCw+ZprM6wa8Xm+5/JjBR7epb+IT5mY6WXOgoon/c9PdfJfFswi3/fFGQy+ +FLK21nAKjEAPXho3fy/CHK3MIon2dMPkQ7aNWlPZkuH8H3J2DwIQeaWieW1GZ50U +64yrIjwrw0P7hHuua0W9YfuPuWt29YpW5g6ilSRE0kdTzoB6TgMzlVRj6RWbxWLX +erwYFesSpLPiQrozK2yywlQsvRV2AxTlf5woJyRTyCqcao5jNZOJJl0mqeGKNKbu +1iYGtZl9aj1XIRxUt+JB2IMKNJasygIp+GRLUDCHKh8RVFwRlVaSNcWbfLDuyNWW +T3lUEjECgYEA84mrs4TLuPfklsQM4WPBdN/2Ud1r0Zn/W8icHcVc/DCFXbcV4aPA +g4yyyyEkyTac2RSbSp+rfUk/pJcG6CVjwaiRIPehdtcLIUP34EdIrwPrPT7/uWVA +o/Hp1ANSILecknQXeE1qDlHVeGAq2k3vAQH2J0m7lMfar7QCBTMTMHcCgYEA8PkO +Uj9+/LoHod2eb4raH29wntis31X5FX/C/8HlmFmQplxfMxpRckzDYQELdHvDggNY +ZQo6pdE22MjCu2bk9AHa2ukMyieWm/mPe46Upr1YV2o5cWnfFFNa/LP2Ii/dWY5V +rFNsHFnrnwcWymX7OKo0Xb8xYnKhKZJAFwSpXxUCgYBPMjXj6wtU20g6vwZxRT9k +AnDXrmmhf7LK5jHefJAAcsbr8t3qwpWYMejypZSQ2nGnJkxZuBLMa0WHAJX+aCpI +j8iiL+USAFxeNPwmswev4lZdVF9Uqtiad9DSYUIT4aHI/nejZ4lVnscMnjlRRIa0 +jS6/F/soJtW2zZLangFfgQKBgCOSAAUwDkSsCThhiGOasXv2bT9laI9HF4+O3m/2 +ZTfJ8Mo91GesuN0Qa77D8rbtFfz5FXFEw0d6zIfPir8y/xTtuSqbQCIPGfJIMl/g +uhyq0oGE0pnlMOLFMyceQXTmb9wqYIchgVHmDBvbZgfWafEBXt1/vYB0v0ltpzw+ +menJAoGBAI0hx3+mrFgA+xJBEk4oexAlro1qbNWoR7BCmLQtd49jG3eZQu4JxWH2 +kh58AIXzLl0X9t4pfMYasYL6jBGvw+AqNdo2krpiL7MWEE8w8FP/wibzqmuloziB +T7BZuCZjpcAM0IxLmQeeUK0LF0mihcqvssxveaet46mj7QoA7bGQ +-----END RSA PRIVATE KEY----- diff --git a/v2rayng/hysteria/core/internal/integration_tests/trafficlogger_test.go b/v2rayng/hysteria/core/internal/integration_tests/trafficlogger_test.go new file mode 100644 index 0000000000..841f4ffa8f --- /dev/null +++ b/v2rayng/hysteria/core/internal/integration_tests/trafficlogger_test.go @@ -0,0 +1,180 @@ +package integration_tests + +import ( + "io" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/apernet/hysteria/core/v2/client" + "github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks" + "github.com/apernet/hysteria/core/v2/server" +) + +// TestClientServerTrafficLoggerTCP tests that the traffic logger is correctly called for TCP connections, +// and that the client is disconnected when the traffic logger returns false. +func TestClientServerTrafficLoggerTCP(t *testing.T) { + // Create server + udpConn, udpAddr, err := serverConn() + assert.NoError(t, err) + serverOb := mocks.NewMockOutbound(t) + auth := mocks.NewMockAuthenticator(t) + auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") + trafficLogger := mocks.NewMockTrafficLogger(t) + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Outbound: serverOb, + Authenticator: auth, + TrafficLogger: trafficLogger, + }) + assert.NoError(t, err) + defer s.Close() + go s.Serve() + + // Create client + trafficLogger.EXPECT().LogOnlineState("nobody", true).Return().Once() + c, _, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + }) + assert.NoError(t, err) + defer c.Close() + + addr := "dontcare.cc:4455" + + sobConn := mocks.NewMockConn(t) + sobConnCh := make(chan []byte, 1) + sobConnChCloseFunc := sync.OnceFunc(func() { close(sobConnCh) }) + sobConn.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) { + b := <-sobConnCh + if b == nil { + return 0, io.EOF + } else { + return copy(bs, b), nil + } + }) + sobConn.EXPECT().Close().RunAndReturn(func() error { + sobConnChCloseFunc() + return nil + }) + serverOb.EXPECT().TCP(addr).Return(sobConn, nil).Once() + trafficLogger.EXPECT().TraceStream(mock.Anything, mock.Anything).Return().Once() + + conn, err := c.TCP(addr) + assert.NoError(t, err) + + // Client reads from server + trafficLogger.EXPECT().LogTraffic("nobody", uint64(0), uint64(11)).Return(true).Once() + sobConnCh <- []byte("knock knock") + buf := make([]byte, 100) + n, err := conn.Read(buf) + assert.NoError(t, err) + assert.Equal(t, 11, n) + assert.Equal(t, "knock knock", string(buf[:n])) + + // Client writes to server + trafficLogger.EXPECT().LogTraffic("nobody", uint64(12), uint64(0)).Return(true).Once() + sobConn.EXPECT().Write([]byte("who is there")).Return(12, nil).Once() + n, err = conn.Write([]byte("who is there")) + assert.NoError(t, err) + assert.Equal(t, 12, n) + time.Sleep(1 * time.Second) // Need some time for the server to receive the data + + // Client reads from server again but blocked + trafficLogger.EXPECT().UntraceStream(mock.Anything).Return().Once() + trafficLogger.EXPECT().LogTraffic("nobody", uint64(0), uint64(4)).Return(false).Once() + trafficLogger.EXPECT().LogOnlineState("nobody", false).Return().Once() + sobConnCh <- []byte("nope") + n, err = conn.Read(buf) + assert.Zero(t, n) + assert.Error(t, err) + + // The client should be disconnected + _, err = c.TCP("whatever") + assert.Error(t, err) +} + +// TestClientServerTrafficLoggerUDP tests that the traffic logger is correctly called for UDP sessions, +// and that the client is disconnected when the traffic logger returns false. +func TestClientServerTrafficLoggerUDP(t *testing.T) { + // Create server + udpConn, udpAddr, err := serverConn() + assert.NoError(t, err) + serverOb := mocks.NewMockOutbound(t) + auth := mocks.NewMockAuthenticator(t) + auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") + trafficLogger := mocks.NewMockTrafficLogger(t) + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Outbound: serverOb, + Authenticator: auth, + TrafficLogger: trafficLogger, + }) + assert.NoError(t, err) + defer s.Close() + go s.Serve() + + // Create client + trafficLogger.EXPECT().LogOnlineState("nobody", true).Return().Once() + c, _, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + }) + assert.NoError(t, err) + defer c.Close() + + addr := "shady.org:43211" + + sobConn := mocks.NewMockUDPConn(t) + sobConnCh := make(chan []byte, 1) + sobConnChCloseFunc := sync.OnceFunc(func() { close(sobConnCh) }) + sobConn.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(bs []byte) (int, string, error) { + b := <-sobConnCh + if b == nil { + return 0, "", io.EOF + } else { + return copy(bs, b), addr, nil + } + }) + sobConn.EXPECT().Close().RunAndReturn(func() error { + sobConnChCloseFunc() + return nil + }) + serverOb.EXPECT().UDP(addr).Return(sobConn, nil).Once() + + conn, err := c.UDP() + assert.NoError(t, err) + + // Client writes to server + trafficLogger.EXPECT().LogTraffic("nobody", uint64(9), uint64(0)).Return(true).Once() + sobConn.EXPECT().WriteTo([]byte("small sad"), addr).Return(9, nil).Once() + err = conn.Send([]byte("small sad"), addr) + assert.NoError(t, err) + time.Sleep(1 * time.Second) // Need some time for the server to receive the data + + // Client reads from server + trafficLogger.EXPECT().LogTraffic("nobody", uint64(0), uint64(7)).Return(true).Once() + sobConnCh <- []byte("big mad") + bs, rAddr, err := conn.Receive() + assert.NoError(t, err) + assert.Equal(t, rAddr, addr) + assert.Equal(t, "big mad", string(bs)) + + // Client reads from server again but blocked + trafficLogger.EXPECT().LogTraffic("nobody", uint64(0), uint64(4)).Return(false).Once() + trafficLogger.EXPECT().LogOnlineState("nobody", false).Return().Once() + sobConnCh <- []byte("nope") + bs, rAddr, err = conn.Receive() + assert.Equal(t, err, io.EOF) + assert.Empty(t, rAddr) + assert.Empty(t, bs) + + // The client should be disconnected + _, err = c.UDP() + assert.Error(t, err) +} diff --git a/v2rayng/hysteria/core/internal/integration_tests/utils_test.go b/v2rayng/hysteria/core/internal/integration_tests/utils_test.go new file mode 100644 index 0000000000..d7ecb68ef3 --- /dev/null +++ b/v2rayng/hysteria/core/internal/integration_tests/utils_test.go @@ -0,0 +1,82 @@ +package integration_tests + +import ( + "crypto/tls" + "io" + "net" + + "github.com/apernet/hysteria/core/v2/server" +) + +// This file provides utilities for the integration tests. + +const ( + testCertFile = "test.crt" + testKeyFile = "test.key" +) + +func serverTLSConfig() server.TLSConfig { + cert, err := tls.LoadX509KeyPair(testCertFile, testKeyFile) + if err != nil { + panic(err) + } + return server.TLSConfig{ + Certificates: []tls.Certificate{cert}, + } +} + +func serverConn() (net.PacketConn, net.Addr, error) { + udpAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514} + udpConn, err := net.ListenUDP("udp", udpAddr) + if err != nil { + return nil, nil, err + } + return udpConn, udpAddr, nil +} + +// tcpEchoServer is a TCP server that echoes what it reads from the connection. +// It will never actively close the connection. +type tcpEchoServer struct { + Listener net.Listener +} + +func (s *tcpEchoServer) Serve() error { + for { + conn, err := s.Listener.Accept() + if err != nil { + return err + } + go func() { + _, _ = io.Copy(conn, conn) + _ = conn.Close() + }() + } +} + +func (s *tcpEchoServer) Close() error { + return s.Listener.Close() +} + +// udpEchoServer is a UDP server that echoes what it reads from the connection. +// It will never actively close the connection. +type udpEchoServer struct { + Conn net.PacketConn +} + +func (s *udpEchoServer) Serve() error { + buf := make([]byte, 65536) + for { + n, addr, err := s.Conn.ReadFrom(buf) + if err != nil { + return err + } + _, err = s.Conn.WriteTo(buf[:n], addr) + if err != nil { + return err + } + } +} + +func (s *udpEchoServer) Close() error { + return s.Conn.Close() +} diff --git a/v2rayng/hysteria/core/internal/pmtud/avail.go b/v2rayng/hysteria/core/internal/pmtud/avail.go new file mode 100644 index 0000000000..cd7afd01ef --- /dev/null +++ b/v2rayng/hysteria/core/internal/pmtud/avail.go @@ -0,0 +1,7 @@ +//go:build linux || windows || darwin + +package pmtud + +const ( + DisablePathMTUDiscovery = false +) diff --git a/v2rayng/hysteria/core/internal/pmtud/unavail.go b/v2rayng/hysteria/core/internal/pmtud/unavail.go new file mode 100644 index 0000000000..917b973aca --- /dev/null +++ b/v2rayng/hysteria/core/internal/pmtud/unavail.go @@ -0,0 +1,13 @@ +//go:build !linux && !windows && !darwin + +package pmtud + +// quic-go's MTU detection is enabled by default on all platforms. +// However, it only actually sets the DF bit on 3 supported platforms (Windows, macOS, Linux). +// As a result, on other platforms, probe packets that should never be fragmented will still +// be fragmented and transmitted. So we're only enabling it for platforms where we've verified +// its functionality for now. + +const ( + DisablePathMTUDiscovery = true +) diff --git a/v2rayng/hysteria/core/internal/protocol/http.go b/v2rayng/hysteria/core/internal/protocol/http.go new file mode 100644 index 0000000000..abcc1a4f46 --- /dev/null +++ b/v2rayng/hysteria/core/internal/protocol/http.go @@ -0,0 +1,68 @@ +package protocol + +import ( + "net/http" + "strconv" +) + +const ( + URLHost = "hysteria" + URLPath = "/auth" + + RequestHeaderAuth = "Hysteria-Auth" + ResponseHeaderUDPEnabled = "Hysteria-UDP" + CommonHeaderCCRX = "Hysteria-CC-RX" + CommonHeaderPadding = "Hysteria-Padding" + + StatusAuthOK = 233 +) + +// AuthRequest is what client sends to server for authentication. +type AuthRequest struct { + Auth string + Rx uint64 // 0 = unknown, client asks server to use bandwidth detection +} + +// AuthResponse is what server sends to client when authentication is passed. +type AuthResponse struct { + UDPEnabled bool + Rx uint64 // 0 = unlimited + RxAuto bool // true = server asks client to use bandwidth detection +} + +func AuthRequestFromHeader(h http.Header) AuthRequest { + rx, _ := strconv.ParseUint(h.Get(CommonHeaderCCRX), 10, 64) + return AuthRequest{ + Auth: h.Get(RequestHeaderAuth), + Rx: rx, + } +} + +func AuthRequestToHeader(h http.Header, req AuthRequest) { + h.Set(RequestHeaderAuth, req.Auth) + h.Set(CommonHeaderCCRX, strconv.FormatUint(req.Rx, 10)) + h.Set(CommonHeaderPadding, authRequestPadding.String()) +} + +func AuthResponseFromHeader(h http.Header) AuthResponse { + resp := AuthResponse{} + resp.UDPEnabled, _ = strconv.ParseBool(h.Get(ResponseHeaderUDPEnabled)) + rxStr := h.Get(CommonHeaderCCRX) + if rxStr == "auto" { + // Special case for server requesting client to use bandwidth detection + resp.RxAuto = true + } else { + resp.Rx, _ = strconv.ParseUint(rxStr, 10, 64) + } + return resp +} + +func AuthResponseToHeader(h http.Header, resp AuthResponse) { + h.Set(ResponseHeaderUDPEnabled, strconv.FormatBool(resp.UDPEnabled)) + if resp.RxAuto { + h.Set(CommonHeaderCCRX, "auto") + } else { + h.Set(CommonHeaderCCRX, strconv.FormatUint(resp.Rx, 10)) + } + h.Set(CommonHeaderPadding, authResponsePadding.String()) +} diff --git a/v2rayng/hysteria/core/internal/protocol/padding.go b/v2rayng/hysteria/core/internal/protocol/padding.go new file mode 100644 index 0000000000..9895cdccea --- /dev/null +++ b/v2rayng/hysteria/core/internal/protocol/padding.go @@ -0,0 +1,31 @@ +package protocol + +import ( + "math/rand" +) + +const ( + paddingChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" +) + +// padding specifies a half-open range [Min, Max). +type padding struct { + Min int + Max int +} + +func (p padding) String() string { + n := p.Min + rand.Intn(p.Max-p.Min) + bs := make([]byte, n) + for i := range bs { + bs[i] = paddingChars[rand.Intn(len(paddingChars))] + } + return string(bs) +} + +var ( + authRequestPadding = padding{Min: 256, Max: 2048} + authResponsePadding = padding{Min: 256, Max: 2048} + tcpRequestPadding = padding{Min: 64, Max: 512} + tcpResponsePadding = padding{Min: 128, Max: 1024} +) diff --git a/v2rayng/hysteria/core/internal/protocol/proxy.go b/v2rayng/hysteria/core/internal/protocol/proxy.go new file mode 100644 index 0000000000..9335c4734f --- /dev/null +++ b/v2rayng/hysteria/core/internal/protocol/proxy.go @@ -0,0 +1,255 @@ +package protocol + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + + "github.com/apernet/hysteria/core/v2/errors" + + "github.com/apernet/quic-go/quicvarint" +) + +const ( + FrameTypeTCPRequest = 0x401 + + // Max length values are for preventing DoS attacks + + MaxAddressLength = 2048 + MaxMessageLength = 2048 + MaxPaddingLength = 4096 + + MaxUDPSize = 4096 + + maxVarInt1 = 63 + maxVarInt2 = 16383 + maxVarInt4 = 1073741823 + maxVarInt8 = 4611686018427387903 +) + +// TCPRequest format: +// 0x401 (QUIC varint) +// Address length (QUIC varint) +// Address (bytes) +// Padding length (QUIC varint) +// Padding (bytes) + +func ReadTCPRequest(r io.Reader) (string, error) { + bReader := quicvarint.NewReader(r) + addrLen, err := quicvarint.Read(bReader) + if err != nil { + return "", err + } + if addrLen == 0 || addrLen > MaxAddressLength { + return "", errors.ProtocolError{Message: "invalid address length"} + } + addrBuf := make([]byte, addrLen) + _, err = io.ReadFull(r, addrBuf) + if err != nil { + return "", err + } + paddingLen, err := quicvarint.Read(bReader) + if err != nil { + return "", err + } + if paddingLen > MaxPaddingLength { + return "", errors.ProtocolError{Message: "invalid padding length"} + } + if paddingLen > 0 { + _, err = io.CopyN(io.Discard, r, int64(paddingLen)) + if err != nil { + return "", err + } + } + return string(addrBuf), nil +} + +func WriteTCPRequest(w io.Writer, addr string) error { + padding := tcpRequestPadding.String() + paddingLen := len(padding) + addrLen := len(addr) + sz := int(quicvarint.Len(FrameTypeTCPRequest)) + + int(quicvarint.Len(uint64(addrLen))) + addrLen + + int(quicvarint.Len(uint64(paddingLen))) + paddingLen + buf := make([]byte, sz) + i := varintPut(buf, FrameTypeTCPRequest) + i += varintPut(buf[i:], uint64(addrLen)) + i += copy(buf[i:], addr) + i += varintPut(buf[i:], uint64(paddingLen)) + copy(buf[i:], padding) + _, err := w.Write(buf) + return err +} + +// TCPResponse format: +// Status (byte, 0=ok, 1=error) +// Message length (QUIC varint) +// Message (bytes) +// Padding length (QUIC varint) +// Padding (bytes) + +func ReadTCPResponse(r io.Reader) (bool, string, error) { + var status [1]byte + if _, err := io.ReadFull(r, status[:]); err != nil { + return false, "", err + } + bReader := quicvarint.NewReader(r) + msgLen, err := quicvarint.Read(bReader) + if err != nil { + return false, "", err + } + if msgLen > MaxMessageLength { + return false, "", errors.ProtocolError{Message: "invalid message length"} + } + var msgBuf []byte + // No message is fine + if msgLen > 0 { + msgBuf = make([]byte, msgLen) + _, err = io.ReadFull(r, msgBuf) + if err != nil { + return false, "", err + } + } + paddingLen, err := quicvarint.Read(bReader) + if err != nil { + return false, "", err + } + if paddingLen > MaxPaddingLength { + return false, "", errors.ProtocolError{Message: "invalid padding length"} + } + if paddingLen > 0 { + _, err = io.CopyN(io.Discard, r, int64(paddingLen)) + if err != nil { + return false, "", err + } + } + return status[0] == 0, string(msgBuf), nil +} + +func WriteTCPResponse(w io.Writer, ok bool, msg string) error { + padding := tcpResponsePadding.String() + paddingLen := len(padding) + msgLen := len(msg) + sz := 1 + int(quicvarint.Len(uint64(msgLen))) + msgLen + + int(quicvarint.Len(uint64(paddingLen))) + paddingLen + buf := make([]byte, sz) + if ok { + buf[0] = 0 + } else { + buf[0] = 1 + } + i := varintPut(buf[1:], uint64(msgLen)) + i += copy(buf[1+i:], msg) + i += varintPut(buf[1+i:], uint64(paddingLen)) + copy(buf[1+i:], padding) + _, err := w.Write(buf) + return err +} + +// UDPMessage format: +// Session ID (uint32 BE) +// Packet ID (uint16 BE) +// Fragment ID (uint8) +// Fragment count (uint8) +// Address length (QUIC varint) +// Address (bytes) +// Data... + +type UDPMessage struct { + SessionID uint32 // 4 + PacketID uint16 // 2 + FragID uint8 // 1 + FragCount uint8 // 1 + Addr string // varint + bytes + Data []byte +} + +func (m *UDPMessage) HeaderSize() int { + lAddr := len(m.Addr) + return 4 + 2 + 1 + 1 + int(quicvarint.Len(uint64(lAddr))) + lAddr +} + +func (m *UDPMessage) Size() int { + return m.HeaderSize() + len(m.Data) +} + +func (m *UDPMessage) Serialize(buf []byte) int { + // Make sure the buffer is big enough + if len(buf) < m.Size() { + return -1 + } + binary.BigEndian.PutUint32(buf, m.SessionID) + binary.BigEndian.PutUint16(buf[4:], m.PacketID) + buf[6] = m.FragID + buf[7] = m.FragCount + i := varintPut(buf[8:], uint64(len(m.Addr))) + i += copy(buf[8+i:], m.Addr) + i += copy(buf[8+i:], m.Data) + return 8 + i +} + +func ParseUDPMessage(msg []byte) (*UDPMessage, error) { + m := &UDPMessage{} + buf := bytes.NewBuffer(msg) + if err := binary.Read(buf, binary.BigEndian, &m.SessionID); err != nil { + return nil, err + } + if err := binary.Read(buf, binary.BigEndian, &m.PacketID); err != nil { + return nil, err + } + if err := binary.Read(buf, binary.BigEndian, &m.FragID); err != nil { + return nil, err + } + if err := binary.Read(buf, binary.BigEndian, &m.FragCount); err != nil { + return nil, err + } + lAddr, err := quicvarint.Read(buf) + if err != nil { + return nil, err + } + if lAddr == 0 || lAddr > MaxMessageLength { + return nil, errors.ProtocolError{Message: "invalid address length"} + } + bs := buf.Bytes() + if len(bs) <= int(lAddr) { + // We use <= instead of < here as we expect at least one byte of data after the address + return nil, errors.ProtocolError{Message: "invalid message length"} + } + m.Addr = string(bs[:lAddr]) + m.Data = bs[lAddr:] + return m, nil +} + +// varintPut is like quicvarint.Append, but instead of appending to a slice, +// it writes to a fixed-size buffer. Returns the number of bytes written. +func varintPut(b []byte, i uint64) int { + if i <= maxVarInt1 { + b[0] = uint8(i) + return 1 + } + if i <= maxVarInt2 { + b[0] = uint8(i>>8) | 0x40 + b[1] = uint8(i) + return 2 + } + if i <= maxVarInt4 { + b[0] = uint8(i>>24) | 0x80 + b[1] = uint8(i >> 16) + b[2] = uint8(i >> 8) + b[3] = uint8(i) + return 4 + } + if i <= maxVarInt8 { + b[0] = uint8(i>>56) | 0xc0 + b[1] = uint8(i >> 48) + b[2] = uint8(i >> 40) + b[3] = uint8(i >> 32) + b[4] = uint8(i >> 24) + b[5] = uint8(i >> 16) + b[6] = uint8(i >> 8) + b[7] = uint8(i) + return 8 + } + panic(fmt.Sprintf("%#x doesn't fit into 62 bits", i)) +} diff --git a/v2rayng/hysteria/core/internal/protocol/proxy_test.go b/v2rayng/hysteria/core/internal/protocol/proxy_test.go new file mode 100644 index 0000000000..111c61587c --- /dev/null +++ b/v2rayng/hysteria/core/internal/protocol/proxy_test.go @@ -0,0 +1,317 @@ +package protocol + +import ( + "bytes" + "reflect" + "strings" + "testing" +) + +func TestUDPMessage(t *testing.T) { + t.Run("buffer too small", func(t *testing.T) { + // Make sure Serialize returns -1 when the buffer is too small. + tBuf := make([]byte, 20) + if (&UDPMessage{ + SessionID: 66, + PacketID: 77, + FragID: 2, + FragCount: 5, + Addr: "random_addr", + Data: []byte("random_data"), + }).Serialize(tBuf) != -1 { + t.Error("Serialize() did not return -1 when the buffer was too small") + } + }) + + type fields struct { + SessionID uint32 + PacketID uint16 + FragID uint8 + FragCount uint8 + Addr string + Data []byte + } + tests := []struct { + name string + fields fields + want []byte + }{ + { + name: "test 1", + fields: fields{ + SessionID: 1, + PacketID: 1, + FragID: 0, + FragCount: 1, + Addr: "example.com:80", + Data: []byte("GET /nothing HTTP/1.1\r\n"), + }, + want: []byte{0x0, 0x0, 0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0xe, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x3a, 0x38, 0x30, 0x47, 0x45, 0x54, 0x20, 0x2f, 0x6e, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x20, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x31, 0x2e, 0x31, 0xd, 0xa}, + }, + { + name: "test 2", + fields: fields{ + SessionID: 1329655244, + Addr: "some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long:9000", + PacketID: 62233, + FragID: 8, + FragCount: 19, + Data: []byte("God is great, beer is good, and people are crazy."), + }, + want: []byte{0x4f, 0x40, 0xed, 0xcc, 0xf3, 0x19, 0x8, 0x13, 0x41, 0xee, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x3a, 0x39, 0x30, 0x30, 0x30, 0x47, 0x6f, 0x64, 0x20, 0x69, 0x73, 0x20, 0x67, 0x72, 0x65, 0x61, 0x74, 0x2c, 0x20, 0x62, 0x65, 0x65, 0x72, 0x20, 0x69, 0x73, 0x20, 0x67, 0x6f, 0x6f, 0x64, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x70, 0x65, 0x6f, 0x70, 0x6c, 0x65, 0x20, 0x61, 0x72, 0x65, 0x20, 0x63, 0x72, 0x61, 0x7a, 0x79, 0x2e}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &UDPMessage{ + SessionID: tt.fields.SessionID, + Addr: tt.fields.Addr, + PacketID: tt.fields.PacketID, + FragID: tt.fields.FragID, + FragCount: tt.fields.FragCount, + Data: tt.fields.Data, + } + // Serialize + buf := make([]byte, MaxUDPSize) + n := m.Serialize(buf) + if got := buf[:n]; !reflect.DeepEqual(got, tt.want) { + t.Errorf("Serialize() = %v, want %v", got, tt.want) + } + // Parse back + if m2, err := ParseUDPMessage(tt.want); err != nil { + t.Errorf("ParseUDPMessage() error = %v", err) + } else { + if !reflect.DeepEqual(m2, m) { + t.Errorf("ParseUDPMessage() = %v, want %v", m2, m) + } + } + }) + } +} + +// TestUDPMessageMalformed is to make sure ParseUDPMessage() fails (but not panic) on malformed data. +func TestUDPMessageMalformed(t *testing.T) { + tests := []struct { + name string + data []byte + }{ + { + name: "empty", + data: []byte{}, + }, + { + name: "zeroes 1", + data: []byte{0, 0, 0, 0}, + }, + { + name: "zeroes 2", + data: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + }, + { + name: "incomplete 1", + data: []byte{0x66, 0xCC, 0xFF, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x55}, + }, + { + name: "incomplete 2", + data: []byte{0x66, 0xCC, 0xFF, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x90, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := ParseUDPMessage(tt.data); err == nil { + t.Errorf("ParseUDPMessage() should fail") + } + }) + } +} + +func TestReadTCPRequest(t *testing.T) { + tests := []struct { + name string + data []byte + want string + wantErr bool + }{ + { + name: "normal no padding", + data: []byte("\x0egoogle.com:443\x00"), + want: "google.com:443", + wantErr: false, + }, + { + name: "normal with padding", + data: []byte("\x0bholy.cc:443\x02gg"), + want: "holy.cc:443", + wantErr: false, + }, + { + name: "incomplete 1", + data: []byte("\x0bhoho"), + want: "", + wantErr: true, + }, + { + name: "incomplete 2", + data: []byte("\x0bholy.cc:443\x05x"), + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := bytes.NewReader(tt.data) + got, err := ReadTCPRequest(r) + if (err != nil) != tt.wantErr { + t.Errorf("ReadTCPRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ReadTCPRequest() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestWriteTCPRequest(t *testing.T) { + tests := []struct { + name string + addr string + wantW string // Just a prefix, we don't care about the padding + wantErr bool + }{ + { + name: "normal 1", + addr: "google.com:443", + wantW: "\x44\x01\x0egoogle.com:443", + wantErr: false, + }, + { + name: "normal 2", + addr: "client-api.arkoselabs.com:8080", + wantW: "\x44\x01\x1eclient-api.arkoselabs.com:8080", + wantErr: false, + }, + { + name: "empty", + addr: "", + wantW: "\x44\x01\x00", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &bytes.Buffer{} + err := WriteTCPRequest(w, tt.addr) + if (err != nil) != tt.wantErr { + t.Errorf("WriteTCPRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotW := w.String(); !(strings.HasPrefix(gotW, tt.wantW) && len(gotW) > len(tt.wantW)) { + t.Errorf("WriteTCPRequest() gotW = %v, want %v", gotW, tt.wantW) + } + }) + } +} + +func TestReadTCPResponse(t *testing.T) { + tests := []struct { + name string + data []byte + want bool + want1 string + wantErr bool + }{ + { + name: "normal ok no padding", + data: []byte("\x00\x0bhello world\x00"), + want: true, + want1: "hello world", + wantErr: false, + }, + { + name: "normal error with padding", + data: []byte("\x01\x06stop!!\x05xxxxx"), + want1: "stop!!", + wantErr: false, + }, + { + name: "normal ok no message with padding", + data: []byte("\x01\x00\x05xxxxx"), + want1: "", + wantErr: false, + }, + { + name: "incomplete 1", + data: []byte("\x00\x0bhoho"), + want1: "", + wantErr: true, + }, + { + name: "incomplete 2", + data: []byte("\x01\x05jesus\x05x"), + want1: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := bytes.NewReader(tt.data) + got, got1, err := ReadTCPResponse(r) + if (err != nil) != tt.wantErr { + t.Errorf("ReadTCPResponse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ReadTCPResponse() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("ReadTCPResponse() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestWriteTCPResponse(t *testing.T) { + type args struct { + ok bool + msg string + } + tests := []struct { + name string + args args + wantW string // Just a prefix, we don't care about the padding + wantErr bool + }{ + { + name: "normal ok", + args: args{ok: true, msg: "hello world"}, + wantW: "\x00\x0bhello world", + wantErr: false, + }, + { + name: "normal error", + args: args{ok: false, msg: "stop!!"}, + wantW: "\x01\x06stop!!", + wantErr: false, + }, + { + name: "empty", + args: args{ok: true, msg: ""}, + wantW: "\x00\x00", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &bytes.Buffer{} + err := WriteTCPResponse(w, tt.args.ok, tt.args.msg) + if (err != nil) != tt.wantErr { + t.Errorf("WriteTCPResponse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotW := w.String(); !(strings.HasPrefix(gotW, tt.wantW) && len(gotW) > len(tt.wantW)) { + t.Errorf("WriteTCPResponse() gotW = %v, want %v", gotW, tt.wantW) + } + }) + } +} diff --git a/v2rayng/hysteria/core/internal/utils/atomic.go b/v2rayng/hysteria/core/internal/utils/atomic.go new file mode 100644 index 0000000000..7739013ec0 --- /dev/null +++ b/v2rayng/hysteria/core/internal/utils/atomic.go @@ -0,0 +1,54 @@ +package utils + +import ( + "sync/atomic" + "time" +) + +type AtomicTime struct { + v atomic.Value +} + +func NewAtomicTime(t time.Time) *AtomicTime { + a := &AtomicTime{} + a.Set(t) + return a +} + +func (t *AtomicTime) Set(new time.Time) { + t.v.Store(new) +} + +func (t *AtomicTime) Get() time.Time { + return t.v.Load().(time.Time) +} + +type Atomic[T any] struct { + v atomic.Value +} + +func (a *Atomic[T]) Load() T { + value := a.v.Load() + if value == nil { + var zero T + return zero + } + return value.(T) +} + +func (a *Atomic[T]) Store(value T) { + a.v.Store(value) +} + +func (a *Atomic[T]) Swap(new T) T { + old := a.v.Swap(new) + if old == nil { + var zero T + return zero + } + return old.(T) +} + +func (a *Atomic[T]) CompareAndSwap(old, new T) bool { + return a.v.CompareAndSwap(old, new) +} diff --git a/v2rayng/hysteria/core/internal/utils/qstream.go b/v2rayng/hysteria/core/internal/utils/qstream.go new file mode 100644 index 0000000000..a3a4833caa --- /dev/null +++ b/v2rayng/hysteria/core/internal/utils/qstream.go @@ -0,0 +1,62 @@ +package utils + +import ( + "context" + "time" + + "github.com/apernet/quic-go" +) + +// QStream is a wrapper of quic.Stream that handles Close() in a way that +// makes more sense to us. By default, quic.Stream's Close() only closes +// the write side of the stream, not the read side. And if there is unread +// data, the stream is not really considered closed until either the data +// is drained or CancelRead() is called. +// References: +// - https://github.com/libp2p/go-libp2p/blob/master/p2p/transport/quic/stream.go +// - https://github.com/quic-go/quic-go/issues/3558 +// - https://github.com/quic-go/quic-go/issues/1599 +type QStream struct { + Stream quic.Stream +} + +func (s *QStream) StreamID() quic.StreamID { + return s.Stream.StreamID() +} + +func (s *QStream) Read(p []byte) (n int, err error) { + return s.Stream.Read(p) +} + +func (s *QStream) CancelRead(code quic.StreamErrorCode) { + s.Stream.CancelRead(code) +} + +func (s *QStream) SetReadDeadline(t time.Time) error { + return s.Stream.SetReadDeadline(t) +} + +func (s *QStream) Write(p []byte) (n int, err error) { + return s.Stream.Write(p) +} + +func (s *QStream) Close() error { + s.Stream.CancelRead(0) + return s.Stream.Close() +} + +func (s *QStream) CancelWrite(code quic.StreamErrorCode) { + s.Stream.CancelWrite(code) +} + +func (s *QStream) Context() context.Context { + return s.Stream.Context() +} + +func (s *QStream) SetWriteDeadline(t time.Time) error { + return s.Stream.SetWriteDeadline(t) +} + +func (s *QStream) SetDeadline(t time.Time) error { + return s.Stream.SetDeadline(t) +} diff --git a/v2rayng/hysteria/core/server/.mockery.yaml b/v2rayng/hysteria/core/server/.mockery.yaml new file mode 100644 index 0000000000..d73136a7eb --- /dev/null +++ b/v2rayng/hysteria/core/server/.mockery.yaml @@ -0,0 +1,15 @@ +with-expecter: true +inpackage: true +dir: . +packages: + github.com/apernet/hysteria/core/v2/server: + interfaces: + udpIO: + config: + mockname: mockUDPIO + udpEventLogger: + config: + mockname: mockUDPEventLogger + UDPConn: + config: + mockname: mockUDPConn diff --git a/v2rayng/hysteria/core/server/config.go b/v2rayng/hysteria/core/server/config.go new file mode 100644 index 0000000000..a01f478f4d --- /dev/null +++ b/v2rayng/hysteria/core/server/config.go @@ -0,0 +1,279 @@ +package server + +import ( + "crypto/tls" + "net" + "net/http" + "sync/atomic" + "time" + + "github.com/apernet/hysteria/core/v2/errors" + "github.com/apernet/hysteria/core/v2/internal/pmtud" + "github.com/apernet/hysteria/core/v2/internal/utils" + "github.com/apernet/quic-go" +) + +const ( + defaultStreamReceiveWindow = 8388608 // 8MB + defaultConnReceiveWindow = defaultStreamReceiveWindow * 5 / 2 // 20MB + defaultMaxIdleTimeout = 30 * time.Second + defaultMaxIncomingStreams = 1024 + defaultUDPIdleTimeout = 60 * time.Second +) + +type Config struct { + TLSConfig TLSConfig + QUICConfig QUICConfig + Conn net.PacketConn + RequestHook RequestHook + Outbound Outbound + BandwidthConfig BandwidthConfig + IgnoreClientBandwidth bool + DisableUDP bool + UDPIdleTimeout time.Duration + Authenticator Authenticator + EventLogger EventLogger + TrafficLogger TrafficLogger + MasqHandler http.Handler +} + +// fill fills the fields that are not set by the user with default values when possible, +// and returns an error if the user has not set a required field, or if a field is invalid. +func (c *Config) fill() error { + if len(c.TLSConfig.Certificates) == 0 && c.TLSConfig.GetCertificate == nil { + return errors.ConfigError{Field: "TLSConfig", Reason: "must set at least one of Certificates or GetCertificate"} + } + if c.QUICConfig.InitialStreamReceiveWindow == 0 { + c.QUICConfig.InitialStreamReceiveWindow = defaultStreamReceiveWindow + } else if c.QUICConfig.InitialStreamReceiveWindow < 16384 { + return errors.ConfigError{Field: "QUICConfig.InitialStreamReceiveWindow", Reason: "must be at least 16384"} + } + if c.QUICConfig.MaxStreamReceiveWindow == 0 { + c.QUICConfig.MaxStreamReceiveWindow = defaultStreamReceiveWindow + } else if c.QUICConfig.MaxStreamReceiveWindow < 16384 { + return errors.ConfigError{Field: "QUICConfig.MaxStreamReceiveWindow", Reason: "must be at least 16384"} + } + if c.QUICConfig.InitialConnectionReceiveWindow == 0 { + c.QUICConfig.InitialConnectionReceiveWindow = defaultConnReceiveWindow + } else if c.QUICConfig.InitialConnectionReceiveWindow < 16384 { + return errors.ConfigError{Field: "QUICConfig.InitialConnectionReceiveWindow", Reason: "must be at least 16384"} + } + if c.QUICConfig.MaxConnectionReceiveWindow == 0 { + c.QUICConfig.MaxConnectionReceiveWindow = defaultConnReceiveWindow + } else if c.QUICConfig.MaxConnectionReceiveWindow < 16384 { + return errors.ConfigError{Field: "QUICConfig.MaxConnectionReceiveWindow", Reason: "must be at least 16384"} + } + if c.QUICConfig.MaxIdleTimeout == 0 { + c.QUICConfig.MaxIdleTimeout = defaultMaxIdleTimeout + } else if c.QUICConfig.MaxIdleTimeout < 4*time.Second || c.QUICConfig.MaxIdleTimeout > 120*time.Second { + return errors.ConfigError{Field: "QUICConfig.MaxIdleTimeout", Reason: "must be between 4s and 120s"} + } + if c.QUICConfig.MaxIncomingStreams == 0 { + c.QUICConfig.MaxIncomingStreams = defaultMaxIncomingStreams + } else if c.QUICConfig.MaxIncomingStreams < 8 { + return errors.ConfigError{Field: "QUICConfig.MaxIncomingStreams", Reason: "must be at least 8"} + } + c.QUICConfig.DisablePathMTUDiscovery = c.QUICConfig.DisablePathMTUDiscovery || pmtud.DisablePathMTUDiscovery + if c.Conn == nil { + return errors.ConfigError{Field: "Conn", Reason: "must be set"} + } + if c.Outbound == nil { + c.Outbound = &defaultOutbound{} + } + if c.BandwidthConfig.MaxTx != 0 && c.BandwidthConfig.MaxTx < 65536 { + return errors.ConfigError{Field: "BandwidthConfig.MaxTx", Reason: "must be at least 65536"} + } + if c.BandwidthConfig.MaxRx != 0 && c.BandwidthConfig.MaxRx < 65536 { + return errors.ConfigError{Field: "BandwidthConfig.MaxRx", Reason: "must be at least 65536"} + } + if c.UDPIdleTimeout == 0 { + c.UDPIdleTimeout = defaultUDPIdleTimeout + } else if c.UDPIdleTimeout < 2*time.Second || c.UDPIdleTimeout > 600*time.Second { + return errors.ConfigError{Field: "UDPIdleTimeout", Reason: "must be between 2s and 600s"} + } + if c.Authenticator == nil { + return errors.ConfigError{Field: "Authenticator", Reason: "must be set"} + } + return nil +} + +// TLSConfig contains the TLS configuration fields that we want to expose to the user. +type TLSConfig struct { + Certificates []tls.Certificate + GetCertificate func(info *tls.ClientHelloInfo) (*tls.Certificate, error) +} + +// QUICConfig contains the QUIC configuration fields that we want to expose to the user. +type QUICConfig struct { + InitialStreamReceiveWindow uint64 + MaxStreamReceiveWindow uint64 + InitialConnectionReceiveWindow uint64 + MaxConnectionReceiveWindow uint64 + MaxIdleTimeout time.Duration + MaxIncomingStreams int64 + DisablePathMTUDiscovery bool // The server may still override this to true on unsupported platforms. +} + +// RequestHook allows filtering and modifying requests before the server connects to the remote. +// A request will only be hooked if Check returns true. +// The returned byte slice, if not empty, will be sent to the remote before proxying - this is +// mainly for "putting back" the content read from the client for sniffing, etc. +// Return a non-nil error to abort the connection. +// Note that due to the current architectural limitations, it can only inspect the first packet +// of a UDP connection. It also cannot put back any data as the first packet is always sent as-is. +type RequestHook interface { + Check(isUDP bool, reqAddr string) bool + TCP(stream quic.Stream, reqAddr *string) ([]byte, error) + UDP(data []byte, reqAddr *string) error +} + +// Outbound provides the implementation of how the server should connect to remote servers. +// Although UDP includes a reqAddr, the implementation does not necessarily have to use it +// to make a "connected" UDP connection that does not accept packets from other addresses. +// In fact, the default implementation simply uses net.ListenUDP for a "full-cone" behavior. +type Outbound interface { + TCP(reqAddr string) (net.Conn, error) + UDP(reqAddr string) (UDPConn, error) +} + +// UDPConn is like net.PacketConn, but uses string for addresses. +type UDPConn interface { + ReadFrom(b []byte) (int, string, error) + WriteTo(b []byte, addr string) (int, error) + Close() error +} + +type defaultOutbound struct{} + +var defaultOutboundDialer = net.Dialer{ + Timeout: 10 * time.Second, +} + +func (o *defaultOutbound) TCP(reqAddr string) (net.Conn, error) { + return defaultOutboundDialer.Dial("tcp", reqAddr) +} + +func (o *defaultOutbound) UDP(reqAddr string) (UDPConn, error) { + conn, err := net.ListenUDP("udp", nil) + if err != nil { + return nil, err + } + return &defaultUDPConn{conn}, nil +} + +type defaultUDPConn struct { + *net.UDPConn +} + +func (c *defaultUDPConn) ReadFrom(b []byte) (int, string, error) { + n, addr, err := c.UDPConn.ReadFrom(b) + if addr != nil { + return n, addr.String(), err + } else { + return n, "", err + } +} + +func (c *defaultUDPConn) WriteTo(b []byte, addr string) (int, error) { + uAddr, err := net.ResolveUDPAddr("udp", addr) + if err != nil { + return 0, err + } + return c.UDPConn.WriteTo(b, uAddr) +} + +// BandwidthConfig describes the maximum bandwidth that the server can use, in bytes per second. +type BandwidthConfig struct { + MaxTx uint64 + MaxRx uint64 +} + +// Authenticator is an interface that provides authentication logic. +type Authenticator interface { + Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) +} + +// EventLogger is an interface that provides logging logic. +type EventLogger interface { + Connect(addr net.Addr, id string, tx uint64) + Disconnect(addr net.Addr, id string, err error) + TCPRequest(addr net.Addr, id, reqAddr string) + TCPError(addr net.Addr, id, reqAddr string, err error) + UDPRequest(addr net.Addr, id string, sessionID uint32, reqAddr string) + UDPError(addr net.Addr, id string, sessionID uint32, err error) +} + +// TrafficLogger is an interface that provides traffic logging logic. +// Tx/Rx in this context refers to the server-remote (proxy target) perspective. +// Tx is the bytes sent from the server to the remote. +// Rx is the bytes received by the server from the remote. +// Apart from logging, the Log function can also return false to signal +// that the client should be disconnected. This can be used to implement +// bandwidth limits or post-connection authentication, for example. +// The implementation of this interface must be thread-safe. +type TrafficLogger interface { + LogTraffic(id string, tx, rx uint64) (ok bool) + LogOnlineState(id string, online bool) + TraceStream(stream quic.Stream, stats *StreamStats) + UntraceStream(stream quic.Stream) +} + +type StreamState int + +const ( + // StreamStateInitial indicates the initial state of a stream. + // Client has opened the stream, but we have not received the proxy request yet. + StreamStateInitial StreamState = iota + + // StreamStateHooking indicates that the hook (usually sniff) is processing. + // Client has sent the proxy request, but sniff requires more data to complete. + StreamStateHooking + + // StreamStateConnecting indicates that we are connecting to the proxy target. + StreamStateConnecting + + // StreamStateEstablished indicates the proxy is established. + StreamStateEstablished + + // StreamStateClosed indicates the stream is closed. + StreamStateClosed +) + +func (s StreamState) String() string { + switch s { + case StreamStateInitial: + return "init" + case StreamStateHooking: + return "hook" + case StreamStateConnecting: + return "connect" + case StreamStateEstablished: + return "estab" + case StreamStateClosed: + return "closed" + default: + return "unknown" + } +} + +type StreamStats struct { + State utils.Atomic[StreamState] + + AuthID string + ConnID uint32 + InitialTime time.Time + + ReqAddr utils.Atomic[string] + HookedReqAddr utils.Atomic[string] + + Tx atomic.Uint64 + Rx atomic.Uint64 + + LastActiveTime utils.Atomic[time.Time] +} + +func (s *StreamStats) setHookedReqAddr(addr string) { + if addr != s.ReqAddr.Load() { + s.HookedReqAddr.Store(addr) + } +} diff --git a/v2rayng/hysteria/core/server/copy.go b/v2rayng/hysteria/core/server/copy.go new file mode 100644 index 0000000000..7123fc89ea --- /dev/null +++ b/v2rayng/hysteria/core/server/copy.go @@ -0,0 +1,69 @@ +package server + +import ( + "errors" + "io" + "time" +) + +var errDisconnect = errors.New("traffic logger requested disconnect") + +func copyBufferLog(dst io.Writer, src io.Reader, log func(n uint64) bool) error { + buf := make([]byte, 32*1024) + for { + nr, er := src.Read(buf) + if nr > 0 { + if !log(uint64(nr)) { + // Log returns false, which means that the client should be disconnected + return errDisconnect + } + _, ew := dst.Write(buf[0:nr]) + if ew != nil { + return ew + } + } + if er != nil { + if er == io.EOF { + // EOF should not be considered as an error + return nil + } + return er + } + } +} + +func copyTwoWayEx(id string, serverRw, remoteRw io.ReadWriter, l TrafficLogger, stats *StreamStats) error { + errChan := make(chan error, 2) + go func() { + errChan <- copyBufferLog(serverRw, remoteRw, func(n uint64) bool { + stats.LastActiveTime.Store(time.Now()) + stats.Rx.Add(n) + return l.LogTraffic(id, 0, n) + }) + }() + go func() { + errChan <- copyBufferLog(remoteRw, serverRw, func(n uint64) bool { + stats.LastActiveTime.Store(time.Now()) + stats.Tx.Add(n) + return l.LogTraffic(id, n, 0) + }) + }() + // Block until one of the two goroutines returns + return <-errChan +} + +// copyTwoWay is the "fast-path" version of copyTwoWayEx that does not log traffic or update stream stats. +// It uses the built-in io.Copy instead of our own copyBufferLog. +func copyTwoWay(serverRw, remoteRw io.ReadWriter) error { + errChan := make(chan error, 2) + go func() { + _, err := io.Copy(serverRw, remoteRw) + errChan <- err + }() + go func() { + _, err := io.Copy(remoteRw, serverRw) + errChan <- err + }() + // Block until one of the two goroutines returns + return <-errChan +} diff --git a/v2rayng/hysteria/core/server/mock_UDPConn.go b/v2rayng/hysteria/core/server/mock_UDPConn.go new file mode 100644 index 0000000000..7299975353 --- /dev/null +++ b/v2rayng/hysteria/core/server/mock_UDPConn.go @@ -0,0 +1,197 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package server + +import mock "github.com/stretchr/testify/mock" + +// mockUDPConn is an autogenerated mock type for the UDPConn type +type mockUDPConn struct { + mock.Mock +} + +type mockUDPConn_Expecter struct { + mock *mock.Mock +} + +func (_m *mockUDPConn) EXPECT() *mockUDPConn_Expecter { + return &mockUDPConn_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function with given fields: +func (_m *mockUDPConn) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockUDPConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type mockUDPConn_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *mockUDPConn_Expecter) Close() *mockUDPConn_Close_Call { + return &mockUDPConn_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *mockUDPConn_Close_Call) Run(run func()) *mockUDPConn_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *mockUDPConn_Close_Call) Return(_a0 error) *mockUDPConn_Close_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockUDPConn_Close_Call) RunAndReturn(run func() error) *mockUDPConn_Close_Call { + _c.Call.Return(run) + return _c +} + +// ReadFrom provides a mock function with given fields: b +func (_m *mockUDPConn) ReadFrom(b []byte) (int, string, error) { + ret := _m.Called(b) + + if len(ret) == 0 { + panic("no return value specified for ReadFrom") + } + + var r0 int + var r1 string + var r2 error + if rf, ok := ret.Get(0).(func([]byte) (int, string, error)); ok { + return rf(b) + } + if rf, ok := ret.Get(0).(func([]byte) int); ok { + r0 = rf(b) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func([]byte) string); ok { + r1 = rf(b) + } else { + r1 = ret.Get(1).(string) + } + + if rf, ok := ret.Get(2).(func([]byte) error); ok { + r2 = rf(b) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// mockUDPConn_ReadFrom_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadFrom' +type mockUDPConn_ReadFrom_Call struct { + *mock.Call +} + +// ReadFrom is a helper method to define mock.On call +// - b []byte +func (_e *mockUDPConn_Expecter) ReadFrom(b interface{}) *mockUDPConn_ReadFrom_Call { + return &mockUDPConn_ReadFrom_Call{Call: _e.mock.On("ReadFrom", b)} +} + +func (_c *mockUDPConn_ReadFrom_Call) Run(run func(b []byte)) *mockUDPConn_ReadFrom_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte)) + }) + return _c +} + +func (_c *mockUDPConn_ReadFrom_Call) Return(_a0 int, _a1 string, _a2 error) *mockUDPConn_ReadFrom_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *mockUDPConn_ReadFrom_Call) RunAndReturn(run func([]byte) (int, string, error)) *mockUDPConn_ReadFrom_Call { + _c.Call.Return(run) + return _c +} + +// WriteTo provides a mock function with given fields: b, addr +func (_m *mockUDPConn) WriteTo(b []byte, addr string) (int, error) { + ret := _m.Called(b, addr) + + if len(ret) == 0 { + panic("no return value specified for WriteTo") + } + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func([]byte, string) (int, error)); ok { + return rf(b, addr) + } + if rf, ok := ret.Get(0).(func([]byte, string) int); ok { + r0 = rf(b, addr) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func([]byte, string) error); ok { + r1 = rf(b, addr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockUDPConn_WriteTo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteTo' +type mockUDPConn_WriteTo_Call struct { + *mock.Call +} + +// WriteTo is a helper method to define mock.On call +// - b []byte +// - addr string +func (_e *mockUDPConn_Expecter) WriteTo(b interface{}, addr interface{}) *mockUDPConn_WriteTo_Call { + return &mockUDPConn_WriteTo_Call{Call: _e.mock.On("WriteTo", b, addr)} +} + +func (_c *mockUDPConn_WriteTo_Call) Run(run func(b []byte, addr string)) *mockUDPConn_WriteTo_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte), args[1].(string)) + }) + return _c +} + +func (_c *mockUDPConn_WriteTo_Call) Return(_a0 int, _a1 error) *mockUDPConn_WriteTo_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockUDPConn_WriteTo_Call) RunAndReturn(run func([]byte, string) (int, error)) *mockUDPConn_WriteTo_Call { + _c.Call.Return(run) + return _c +} + +// newMockUDPConn creates a new instance of mockUDPConn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockUDPConn(t interface { + mock.TestingT + Cleanup(func()) +}) *mockUDPConn { + mock := &mockUDPConn{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/v2rayng/hysteria/core/server/mock_udpEventLogger.go b/v2rayng/hysteria/core/server/mock_udpEventLogger.go new file mode 100644 index 0000000000..5a54b0ba6a --- /dev/null +++ b/v2rayng/hysteria/core/server/mock_udpEventLogger.go @@ -0,0 +1,100 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package server + +import mock "github.com/stretchr/testify/mock" + +// mockUDPEventLogger is an autogenerated mock type for the udpEventLogger type +type mockUDPEventLogger struct { + mock.Mock +} + +type mockUDPEventLogger_Expecter struct { + mock *mock.Mock +} + +func (_m *mockUDPEventLogger) EXPECT() *mockUDPEventLogger_Expecter { + return &mockUDPEventLogger_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function with given fields: sessionID, err +func (_m *mockUDPEventLogger) Close(sessionID uint32, err error) { + _m.Called(sessionID, err) +} + +// mockUDPEventLogger_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type mockUDPEventLogger_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +// - sessionID uint32 +// - err error +func (_e *mockUDPEventLogger_Expecter) Close(sessionID interface{}, err interface{}) *mockUDPEventLogger_Close_Call { + return &mockUDPEventLogger_Close_Call{Call: _e.mock.On("Close", sessionID, err)} +} + +func (_c *mockUDPEventLogger_Close_Call) Run(run func(sessionID uint32, err error)) *mockUDPEventLogger_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(uint32), args[1].(error)) + }) + return _c +} + +func (_c *mockUDPEventLogger_Close_Call) Return() *mockUDPEventLogger_Close_Call { + _c.Call.Return() + return _c +} + +func (_c *mockUDPEventLogger_Close_Call) RunAndReturn(run func(uint32, error)) *mockUDPEventLogger_Close_Call { + _c.Call.Return(run) + return _c +} + +// New provides a mock function with given fields: sessionID, reqAddr +func (_m *mockUDPEventLogger) New(sessionID uint32, reqAddr string) { + _m.Called(sessionID, reqAddr) +} + +// mockUDPEventLogger_New_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'New' +type mockUDPEventLogger_New_Call struct { + *mock.Call +} + +// New is a helper method to define mock.On call +// - sessionID uint32 +// - reqAddr string +func (_e *mockUDPEventLogger_Expecter) New(sessionID interface{}, reqAddr interface{}) *mockUDPEventLogger_New_Call { + return &mockUDPEventLogger_New_Call{Call: _e.mock.On("New", sessionID, reqAddr)} +} + +func (_c *mockUDPEventLogger_New_Call) Run(run func(sessionID uint32, reqAddr string)) *mockUDPEventLogger_New_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(uint32), args[1].(string)) + }) + return _c +} + +func (_c *mockUDPEventLogger_New_Call) Return() *mockUDPEventLogger_New_Call { + _c.Call.Return() + return _c +} + +func (_c *mockUDPEventLogger_New_Call) RunAndReturn(run func(uint32, string)) *mockUDPEventLogger_New_Call { + _c.Call.Return(run) + return _c +} + +// newMockUDPEventLogger creates a new instance of mockUDPEventLogger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockUDPEventLogger(t interface { + mock.TestingT + Cleanup(func()) +}) *mockUDPEventLogger { + mock := &mockUDPEventLogger{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/v2rayng/hysteria/core/server/mock_udpIO.go b/v2rayng/hysteria/core/server/mock_udpIO.go new file mode 100644 index 0000000000..bbdcf94d4c --- /dev/null +++ b/v2rayng/hysteria/core/server/mock_udpIO.go @@ -0,0 +1,244 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package server + +import ( + protocol "github.com/apernet/hysteria/core/v2/internal/protocol" + mock "github.com/stretchr/testify/mock" +) + +// mockUDPIO is an autogenerated mock type for the udpIO type +type mockUDPIO struct { + mock.Mock +} + +type mockUDPIO_Expecter struct { + mock *mock.Mock +} + +func (_m *mockUDPIO) EXPECT() *mockUDPIO_Expecter { + return &mockUDPIO_Expecter{mock: &_m.Mock} +} + +// Hook provides a mock function with given fields: data, reqAddr +func (_m *mockUDPIO) Hook(data []byte, reqAddr *string) error { + ret := _m.Called(data, reqAddr) + + if len(ret) == 0 { + panic("no return value specified for Hook") + } + + var r0 error + if rf, ok := ret.Get(0).(func([]byte, *string) error); ok { + r0 = rf(data, reqAddr) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockUDPIO_Hook_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Hook' +type mockUDPIO_Hook_Call struct { + *mock.Call +} + +// Hook is a helper method to define mock.On call +// - data []byte +// - reqAddr *string +func (_e *mockUDPIO_Expecter) Hook(data interface{}, reqAddr interface{}) *mockUDPIO_Hook_Call { + return &mockUDPIO_Hook_Call{Call: _e.mock.On("Hook", data, reqAddr)} +} + +func (_c *mockUDPIO_Hook_Call) Run(run func(data []byte, reqAddr *string)) *mockUDPIO_Hook_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte), args[1].(*string)) + }) + return _c +} + +func (_c *mockUDPIO_Hook_Call) Return(_a0 error) *mockUDPIO_Hook_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockUDPIO_Hook_Call) RunAndReturn(run func([]byte, *string) error) *mockUDPIO_Hook_Call { + _c.Call.Return(run) + return _c +} + +// ReceiveMessage provides a mock function with given fields: +func (_m *mockUDPIO) ReceiveMessage() (*protocol.UDPMessage, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ReceiveMessage") + } + + var r0 *protocol.UDPMessage + var r1 error + if rf, ok := ret.Get(0).(func() (*protocol.UDPMessage, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *protocol.UDPMessage); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*protocol.UDPMessage) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockUDPIO_ReceiveMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReceiveMessage' +type mockUDPIO_ReceiveMessage_Call struct { + *mock.Call +} + +// ReceiveMessage is a helper method to define mock.On call +func (_e *mockUDPIO_Expecter) ReceiveMessage() *mockUDPIO_ReceiveMessage_Call { + return &mockUDPIO_ReceiveMessage_Call{Call: _e.mock.On("ReceiveMessage")} +} + +func (_c *mockUDPIO_ReceiveMessage_Call) Run(run func()) *mockUDPIO_ReceiveMessage_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *mockUDPIO_ReceiveMessage_Call) Return(_a0 *protocol.UDPMessage, _a1 error) *mockUDPIO_ReceiveMessage_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockUDPIO_ReceiveMessage_Call) RunAndReturn(run func() (*protocol.UDPMessage, error)) *mockUDPIO_ReceiveMessage_Call { + _c.Call.Return(run) + return _c +} + +// SendMessage provides a mock function with given fields: _a0, _a1 +func (_m *mockUDPIO) SendMessage(_a0 []byte, _a1 *protocol.UDPMessage) error { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for SendMessage") + } + + var r0 error + if rf, ok := ret.Get(0).(func([]byte, *protocol.UDPMessage) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockUDPIO_SendMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMessage' +type mockUDPIO_SendMessage_Call struct { + *mock.Call +} + +// SendMessage is a helper method to define mock.On call +// - _a0 []byte +// - _a1 *protocol.UDPMessage +func (_e *mockUDPIO_Expecter) SendMessage(_a0 interface{}, _a1 interface{}) *mockUDPIO_SendMessage_Call { + return &mockUDPIO_SendMessage_Call{Call: _e.mock.On("SendMessage", _a0, _a1)} +} + +func (_c *mockUDPIO_SendMessage_Call) Run(run func(_a0 []byte, _a1 *protocol.UDPMessage)) *mockUDPIO_SendMessage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte), args[1].(*protocol.UDPMessage)) + }) + return _c +} + +func (_c *mockUDPIO_SendMessage_Call) Return(_a0 error) *mockUDPIO_SendMessage_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockUDPIO_SendMessage_Call) RunAndReturn(run func([]byte, *protocol.UDPMessage) error) *mockUDPIO_SendMessage_Call { + _c.Call.Return(run) + return _c +} + +// UDP provides a mock function with given fields: reqAddr +func (_m *mockUDPIO) UDP(reqAddr string) (UDPConn, error) { + ret := _m.Called(reqAddr) + + if len(ret) == 0 { + panic("no return value specified for UDP") + } + + var r0 UDPConn + var r1 error + if rf, ok := ret.Get(0).(func(string) (UDPConn, error)); ok { + return rf(reqAddr) + } + if rf, ok := ret.Get(0).(func(string) UDPConn); ok { + r0 = rf(reqAddr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(UDPConn) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(reqAddr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockUDPIO_UDP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDP' +type mockUDPIO_UDP_Call struct { + *mock.Call +} + +// UDP is a helper method to define mock.On call +// - reqAddr string +func (_e *mockUDPIO_Expecter) UDP(reqAddr interface{}) *mockUDPIO_UDP_Call { + return &mockUDPIO_UDP_Call{Call: _e.mock.On("UDP", reqAddr)} +} + +func (_c *mockUDPIO_UDP_Call) Run(run func(reqAddr string)) *mockUDPIO_UDP_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *mockUDPIO_UDP_Call) Return(_a0 UDPConn, _a1 error) *mockUDPIO_UDP_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockUDPIO_UDP_Call) RunAndReturn(run func(string) (UDPConn, error)) *mockUDPIO_UDP_Call { + _c.Call.Return(run) + return _c +} + +// newMockUDPIO creates a new instance of mockUDPIO. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockUDPIO(t interface { + mock.TestingT + Cleanup(func()) +}) *mockUDPIO { + mock := &mockUDPIO{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/v2rayng/hysteria/core/server/server.go b/v2rayng/hysteria/core/server/server.go new file mode 100644 index 0000000000..696f1d0956 --- /dev/null +++ b/v2rayng/hysteria/core/server/server.go @@ -0,0 +1,387 @@ +package server + +import ( + "context" + "crypto/tls" + "math/rand" + "net/http" + "sync" + "time" + + "github.com/apernet/quic-go" + "github.com/apernet/quic-go/http3" + + "github.com/apernet/hysteria/core/v2/internal/congestion" + "github.com/apernet/hysteria/core/v2/internal/protocol" + "github.com/apernet/hysteria/core/v2/internal/utils" +) + +const ( + closeErrCodeOK = 0x100 // HTTP3 ErrCodeNoError + closeErrCodeTrafficLimitReached = 0x107 // HTTP3 ErrCodeExcessiveLoad +) + +type Server interface { + Serve() error + Close() error +} + +func NewServer(config *Config) (Server, error) { + if err := config.fill(); err != nil { + return nil, err + } + tlsConfig := http3.ConfigureTLSConfig(&tls.Config{ + Certificates: config.TLSConfig.Certificates, + GetCertificate: config.TLSConfig.GetCertificate, + }) + quicConfig := &quic.Config{ + InitialStreamReceiveWindow: config.QUICConfig.InitialStreamReceiveWindow, + MaxStreamReceiveWindow: config.QUICConfig.MaxStreamReceiveWindow, + InitialConnectionReceiveWindow: config.QUICConfig.InitialConnectionReceiveWindow, + MaxConnectionReceiveWindow: config.QUICConfig.MaxConnectionReceiveWindow, + MaxIdleTimeout: config.QUICConfig.MaxIdleTimeout, + MaxIncomingStreams: config.QUICConfig.MaxIncomingStreams, + DisablePathMTUDiscovery: config.QUICConfig.DisablePathMTUDiscovery, + EnableDatagrams: true, + } + listener, err := quic.Listen(config.Conn, tlsConfig, quicConfig) + if err != nil { + _ = config.Conn.Close() + return nil, err + } + return &serverImpl{ + config: config, + listener: listener, + }, nil +} + +type serverImpl struct { + config *Config + listener *quic.Listener +} + +func (s *serverImpl) Serve() error { + for { + conn, err := s.listener.Accept(context.Background()) + if err != nil { + return err + } + go s.handleClient(conn) + } +} + +func (s *serverImpl) Close() error { + err := s.listener.Close() + _ = s.config.Conn.Close() + return err +} + +func (s *serverImpl) handleClient(conn quic.Connection) { + handler := newH3sHandler(s.config, conn) + h3s := http3.Server{ + Handler: handler, + StreamHijacker: handler.ProxyStreamHijacker, + } + err := h3s.ServeQUICConn(conn) + // If the client is authenticated, we need to log the disconnect event + if handler.authenticated { + if tl := s.config.TrafficLogger; tl != nil { + tl.LogOnlineState(handler.authID, false) + } + if el := s.config.EventLogger; el != nil { + el.Disconnect(conn.RemoteAddr(), handler.authID, err) + } + } + _ = conn.CloseWithError(closeErrCodeOK, "") +} + +type h3sHandler struct { + config *Config + conn quic.Connection + + authenticated bool + authMutex sync.Mutex + authID string + connID uint32 // a random id for dump streams + + udpSM *udpSessionManager // Only set after authentication +} + +func newH3sHandler(config *Config, conn quic.Connection) *h3sHandler { + return &h3sHandler{ + config: config, + conn: conn, + connID: rand.Uint32(), + } +} + +func (h *h3sHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost && r.Host == protocol.URLHost && r.URL.Path == protocol.URLPath { + h.authMutex.Lock() + defer h.authMutex.Unlock() + if h.authenticated { + // Already authenticated + protocol.AuthResponseToHeader(w.Header(), protocol.AuthResponse{ + UDPEnabled: !h.config.DisableUDP, + Rx: h.config.BandwidthConfig.MaxRx, + RxAuto: h.config.IgnoreClientBandwidth, + }) + w.WriteHeader(protocol.StatusAuthOK) + return + } + authReq := protocol.AuthRequestFromHeader(r.Header) + actualTx := authReq.Rx + ok, id := h.config.Authenticator.Authenticate(h.conn.RemoteAddr(), authReq.Auth, actualTx) + if ok { + // Set authenticated flag + h.authenticated = true + h.authID = id + if h.config.IgnoreClientBandwidth { + // Ignore client bandwidth, always use BBR + congestion.UseBBR(h.conn) + actualTx = 0 + } else { + // actualTx = min(serverTx, clientRx) + if h.config.BandwidthConfig.MaxTx > 0 && actualTx > h.config.BandwidthConfig.MaxTx { + // We have a maxTx limit and the client is asking for more than that, + // return and use the limit instead + actualTx = h.config.BandwidthConfig.MaxTx + } + if actualTx > 0 { + congestion.UseBrutal(h.conn, actualTx) + } else { + // Client doesn't know its own bandwidth, use BBR + congestion.UseBBR(h.conn) + } + } + // Auth OK, send response + protocol.AuthResponseToHeader(w.Header(), protocol.AuthResponse{ + UDPEnabled: !h.config.DisableUDP, + Rx: h.config.BandwidthConfig.MaxRx, + RxAuto: h.config.IgnoreClientBandwidth, + }) + w.WriteHeader(protocol.StatusAuthOK) + // Call event logger + if tl := h.config.TrafficLogger; tl != nil { + tl.LogOnlineState(id, true) + } + if el := h.config.EventLogger; el != nil { + el.Connect(h.conn.RemoteAddr(), id, actualTx) + } + // Initialize UDP session manager (if UDP is enabled) + // We use sync.Once to make sure that only one goroutine is started, + // as ServeHTTP may be called by multiple goroutines simultaneously + if !h.config.DisableUDP { + go func() { + sm := newUDPSessionManager( + &udpIOImpl{h.conn, id, h.config.TrafficLogger, h.config.RequestHook, h.config.Outbound}, + &udpEventLoggerImpl{h.conn, id, h.config.EventLogger}, + h.config.UDPIdleTimeout) + h.udpSM = sm + go sm.Run() + }() + } + } else { + // Auth failed, pretend to be a normal HTTP server + h.masqHandler(w, r) + } + } else { + // Not an auth request, pretend to be a normal HTTP server + h.masqHandler(w, r) + } +} + +func (h *h3sHandler) ProxyStreamHijacker(ft http3.FrameType, id quic.ConnectionTracingID, stream quic.Stream, err error) (bool, error) { + if err != nil || !h.authenticated { + return false, nil + } + + // Wraps the stream with QStream, which handles Close() properly + stream = &utils.QStream{Stream: stream} + + switch ft { + case protocol.FrameTypeTCPRequest: + go h.handleTCPRequest(stream) + return true, nil + default: + return false, nil + } +} + +func (h *h3sHandler) handleTCPRequest(stream quic.Stream) { + trafficLogger := h.config.TrafficLogger + streamStats := &StreamStats{ + AuthID: h.authID, + ConnID: h.connID, + InitialTime: time.Now(), + } + streamStats.State.Store(StreamStateInitial) + streamStats.LastActiveTime.Store(time.Now()) + defer func() { + streamStats.State.Store(StreamStateClosed) + }() + if trafficLogger != nil { + trafficLogger.TraceStream(stream, streamStats) + defer trafficLogger.UntraceStream(stream) + } + + // Read request + reqAddr, err := protocol.ReadTCPRequest(stream) + if err != nil { + _ = stream.Close() + return + } + streamStats.ReqAddr.Store(reqAddr) + // Call the hook if set + var putback []byte + var hooked bool + if h.config.RequestHook != nil { + hooked = h.config.RequestHook.Check(false, reqAddr) + // When the hook is enabled, the server should always accept a connection + // so that the client will send whatever request the hook wants to see. + // This is essentially a server-side fast-open. + if hooked { + streamStats.State.Store(StreamStateHooking) + _ = protocol.WriteTCPResponse(stream, true, "RequestHook enabled") + putback, err = h.config.RequestHook.TCP(stream, &reqAddr) + if err != nil { + _ = stream.Close() + return + } + streamStats.setHookedReqAddr(reqAddr) + } + } + // Log the event + if h.config.EventLogger != nil { + h.config.EventLogger.TCPRequest(h.conn.RemoteAddr(), h.authID, reqAddr) + } + // Dial target + streamStats.State.Store(StreamStateConnecting) + tConn, err := h.config.Outbound.TCP(reqAddr) + if err != nil { + if !hooked { + _ = protocol.WriteTCPResponse(stream, false, err.Error()) + } + _ = stream.Close() + // Log the error + if h.config.EventLogger != nil { + h.config.EventLogger.TCPError(h.conn.RemoteAddr(), h.authID, reqAddr, err) + } + return + } + if !hooked { + _ = protocol.WriteTCPResponse(stream, true, "Connected") + } + streamStats.State.Store(StreamStateEstablished) + // Put back the data if the hook requested + if len(putback) > 0 { + n, _ := tConn.Write(putback) + streamStats.Tx.Add(uint64(n)) + } + // Start proxying + if trafficLogger != nil { + err = copyTwoWayEx(h.authID, stream, tConn, trafficLogger, streamStats) + } else { + // Use the fast path if no traffic logger is set + err = copyTwoWay(stream, tConn) + } + if h.config.EventLogger != nil { + h.config.EventLogger.TCPError(h.conn.RemoteAddr(), h.authID, reqAddr, err) + } + // Cleanup + _ = tConn.Close() + _ = stream.Close() + // Disconnect the client if TrafficLogger requested + if err == errDisconnect { + _ = h.conn.CloseWithError(closeErrCodeTrafficLimitReached, "") + } +} + +func (h *h3sHandler) masqHandler(w http.ResponseWriter, r *http.Request) { + if h.config.MasqHandler != nil { + h.config.MasqHandler.ServeHTTP(w, r) + } else { + // Return 404 for everything + http.NotFound(w, r) + } +} + +// udpIOImpl is the IO implementation for udpSessionManager with TrafficLogger support +type udpIOImpl struct { + Conn quic.Connection + AuthID string + TrafficLogger TrafficLogger + RequestHook RequestHook + Outbound Outbound +} + +func (io *udpIOImpl) ReceiveMessage() (*protocol.UDPMessage, error) { + for { + msg, err := io.Conn.ReceiveDatagram(context.Background()) + if err != nil { + // Connection error, this will stop the session manager + return nil, err + } + udpMsg, err := protocol.ParseUDPMessage(msg) + if err != nil { + // Invalid message, this is fine - just wait for the next + continue + } + if io.TrafficLogger != nil { + ok := io.TrafficLogger.LogTraffic(io.AuthID, uint64(len(udpMsg.Data)), 0) + if !ok { + // TrafficLogger requested to disconnect the client + _ = io.Conn.CloseWithError(closeErrCodeTrafficLimitReached, "") + return nil, errDisconnect + } + } + return udpMsg, nil + } +} + +func (io *udpIOImpl) SendMessage(buf []byte, msg *protocol.UDPMessage) error { + if io.TrafficLogger != nil { + ok := io.TrafficLogger.LogTraffic(io.AuthID, 0, uint64(len(msg.Data))) + if !ok { + // TrafficLogger requested to disconnect the client + _ = io.Conn.CloseWithError(closeErrCodeTrafficLimitReached, "") + return errDisconnect + } + } + msgN := msg.Serialize(buf) + if msgN < 0 { + // Message larger than buffer, silent drop + return nil + } + return io.Conn.SendDatagram(buf[:msgN]) +} + +func (io *udpIOImpl) Hook(data []byte, reqAddr *string) error { + if io.RequestHook != nil && io.RequestHook.Check(true, *reqAddr) { + return io.RequestHook.UDP(data, reqAddr) + } else { + return nil + } +} + +func (io *udpIOImpl) UDP(reqAddr string) (UDPConn, error) { + return io.Outbound.UDP(reqAddr) +} + +type udpEventLoggerImpl struct { + Conn quic.Connection + AuthID string + EventLogger EventLogger +} + +func (l *udpEventLoggerImpl) New(sessionID uint32, reqAddr string) { + if l.EventLogger != nil { + l.EventLogger.UDPRequest(l.Conn.RemoteAddr(), l.AuthID, sessionID, reqAddr) + } +} + +func (l *udpEventLoggerImpl) Close(sessionID uint32, err error) { + if l.EventLogger != nil { + l.EventLogger.UDPError(l.Conn.RemoteAddr(), l.AuthID, sessionID, err) + } +} diff --git a/v2rayng/hysteria/core/server/udp.go b/v2rayng/hysteria/core/server/udp.go new file mode 100644 index 0000000000..14efc9e592 --- /dev/null +++ b/v2rayng/hysteria/core/server/udp.go @@ -0,0 +1,329 @@ +package server + +import ( + "errors" + "math/rand" + "sync" + "time" + + "github.com/apernet/quic-go" + + "github.com/apernet/hysteria/core/v2/internal/frag" + "github.com/apernet/hysteria/core/v2/internal/protocol" + "github.com/apernet/hysteria/core/v2/internal/utils" +) + +const ( + idleCleanupInterval = 1 * time.Second +) + +type udpIO interface { + ReceiveMessage() (*protocol.UDPMessage, error) + SendMessage([]byte, *protocol.UDPMessage) error + Hook(data []byte, reqAddr *string) error + UDP(reqAddr string) (UDPConn, error) +} + +type udpEventLogger interface { + New(sessionID uint32, reqAddr string) + Close(sessionID uint32, err error) +} + +type udpSessionEntry struct { + ID uint32 + OverrideAddr string // Ignore the address in the UDP message, always use this if not empty + OriginalAddr string // The original address in the UDP message + D *frag.Defragger + Last *utils.AtomicTime + IO udpIO + + DialFunc func(addr string, firstMsgData []byte) (conn UDPConn, actualAddr string, err error) + ExitFunc func(err error) + + conn UDPConn + connLock sync.Mutex + closed bool +} + +func newUDPSessionEntry( + id uint32, io udpIO, + dialFunc func(string, []byte) (UDPConn, string, error), + exitFunc func(error), +) (e *udpSessionEntry) { + e = &udpSessionEntry{ + ID: id, + D: &frag.Defragger{}, + Last: utils.NewAtomicTime(time.Now()), + IO: io, + + DialFunc: dialFunc, + ExitFunc: exitFunc, + } + + return +} + +// CloseWithErr closes the session and calls ExitFunc with the given error. +// A nil error indicates the session is cleaned up due to timeout. +func (e *udpSessionEntry) CloseWithErr(err error) { + // We need this lock to ensure not to create conn after session exit + e.connLock.Lock() + + if e.closed { + // Already closed + e.connLock.Unlock() + return + } + + e.closed = true + if e.conn != nil { + _ = e.conn.Close() + } + e.connLock.Unlock() + + e.ExitFunc(err) +} + +// Feed feeds a UDP message to the session. +// If the message itself is a complete message, or it completes a fragmented message, +// the message is written to the session's UDP connection, and the number of bytes +// written is returned. +// Otherwise, 0 and nil are returned. +func (e *udpSessionEntry) Feed(msg *protocol.UDPMessage) (int, error) { + e.Last.Set(time.Now()) + dfMsg := e.D.Feed(msg) + if dfMsg == nil { + return 0, nil + } + + if e.conn == nil { + err := e.initConn(dfMsg) + if err != nil { + return 0, err + } + } + + addr := dfMsg.Addr + if e.OverrideAddr != "" { + addr = e.OverrideAddr + } + + return e.conn.WriteTo(dfMsg.Data, addr) +} + +// initConn initializes the UDP connection of the session. +// If no error is returned, the e.conn is set to the new connection. +func (e *udpSessionEntry) initConn(firstMsg *protocol.UDPMessage) error { + // We need this lock to ensure not to create conn after session exit + e.connLock.Lock() + + if e.closed { + e.connLock.Unlock() + return errors.New("session is closed") + } + + conn, actualAddr, err := e.DialFunc(firstMsg.Addr, firstMsg.Data) + if err != nil { + // Fail fast if DialFunc failed + // (usually indicates the connection has been rejected by the ACL) + e.connLock.Unlock() + // CloseWithErr acquires the connLock again + e.CloseWithErr(err) + return err + } + + e.conn = conn + + if firstMsg.Addr != actualAddr { + // Hook changed the address, enable address override + e.OverrideAddr = actualAddr + e.OriginalAddr = firstMsg.Addr + } + go e.receiveLoop() + + e.connLock.Unlock() + return nil +} + +// receiveLoop receives incoming UDP packets, packs them into UDP messages, +// and sends using the IO. +// Exit when either the underlying UDP connection returns error (e.g. closed), +// or the IO returns error when sending. +func (e *udpSessionEntry) receiveLoop() { + udpBuf := make([]byte, protocol.MaxUDPSize) + msgBuf := make([]byte, protocol.MaxUDPSize) + for { + udpN, rAddr, err := e.conn.ReadFrom(udpBuf) + if err != nil { + e.CloseWithErr(err) + return + } + e.Last.Set(time.Now()) + + if e.OriginalAddr != "" { + // Use the original address in the opposite direction, + // otherwise the QUIC clients or NAT on the client side + // may not treat it as the same UDP session. + rAddr = e.OriginalAddr + } + + msg := &protocol.UDPMessage{ + SessionID: e.ID, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: rAddr, + Data: udpBuf[:udpN], + } + err = sendMessageAutoFrag(e.IO, msgBuf, msg) + if err != nil { + e.CloseWithErr(err) + return + } + } +} + +// sendMessageAutoFrag tries to send a UDP message as a whole first, +// but if it fails due to quic.ErrMessageTooLarge, it tries again by +// fragmenting the message. +func sendMessageAutoFrag(io udpIO, buf []byte, msg *protocol.UDPMessage) error { + err := io.SendMessage(buf, msg) + var errTooLarge *quic.DatagramTooLargeError + if errors.As(err, &errTooLarge) { + // Message too large, try fragmentation + msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1 + fMsgs := frag.FragUDPMessage(msg, int(errTooLarge.MaxDataLen)) + for _, fMsg := range fMsgs { + err := io.SendMessage(buf, &fMsg) + if err != nil { + return err + } + } + return nil + } else { + return err + } +} + +// udpSessionManager manages the lifecycle of UDP sessions. +// Each UDP session is identified by a SessionID, and corresponds to a UDP connection. +// A UDP session is created when a UDP message with a new SessionID is received. +// Similar to standard NAT, a UDP session is destroyed when no UDP message is received +// for a certain period of time (specified by idleTimeout). +type udpSessionManager struct { + io udpIO + eventLogger udpEventLogger + idleTimeout time.Duration + + mutex sync.RWMutex + m map[uint32]*udpSessionEntry +} + +func newUDPSessionManager(io udpIO, eventLogger udpEventLogger, idleTimeout time.Duration) *udpSessionManager { + return &udpSessionManager{ + io: io, + eventLogger: eventLogger, + idleTimeout: idleTimeout, + m: make(map[uint32]*udpSessionEntry), + } +} + +// Run runs the session manager main loop. +// Exit and returns error when the underlying io returns error (e.g. closed). +func (m *udpSessionManager) Run() error { + stopCh := make(chan struct{}) + go m.idleCleanupLoop(stopCh) + defer close(stopCh) + defer m.cleanup(false) + + for { + msg, err := m.io.ReceiveMessage() + if err != nil { + return err + } + m.feed(msg) + } +} + +func (m *udpSessionManager) idleCleanupLoop(stopCh <-chan struct{}) { + ticker := time.NewTicker(idleCleanupInterval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + m.cleanup(true) + case <-stopCh: + return + } + } +} + +func (m *udpSessionManager) cleanup(idleOnly bool) { + timeoutEntry := make([]*udpSessionEntry, 0, len(m.m)) + + // We use RLock here as we are only scanning the map, not deleting from it. + m.mutex.RLock() + now := time.Now() + for _, entry := range m.m { + if !idleOnly || now.Sub(entry.Last.Get()) > m.idleTimeout { + timeoutEntry = append(timeoutEntry, entry) + } + } + m.mutex.RUnlock() + + for _, entry := range timeoutEntry { + // This eventually calls entry.ExitFunc, + // where the m.mutex will be locked again to remove the entry from the map. + entry.CloseWithErr(nil) + } +} + +func (m *udpSessionManager) feed(msg *protocol.UDPMessage) { + m.mutex.RLock() + entry := m.m[msg.SessionID] + m.mutex.RUnlock() + + // Create a new session if not exists + if entry == nil { + dialFunc := func(addr string, firstMsgData []byte) (conn UDPConn, actualAddr string, err error) { + // Call the hook + err = m.io.Hook(firstMsgData, &addr) + if err != nil { + return + } + actualAddr = addr + // Log the event + m.eventLogger.New(msg.SessionID, addr) + // Dial target + conn, err = m.io.UDP(addr) + return + } + exitFunc := func(err error) { + // Log the event + m.eventLogger.Close(entry.ID, err) + + // Remove the session from the map + m.mutex.Lock() + delete(m.m, entry.ID) + m.mutex.Unlock() + } + + entry = newUDPSessionEntry(msg.SessionID, m.io, dialFunc, exitFunc) + + // Insert the session into the map + m.mutex.Lock() + m.m[msg.SessionID] = entry + m.mutex.Unlock() + } + + // Feed the message to the session + // Feed (send) errors are ignored for now, + // as some are temporary (e.g. invalid address) + _, _ = entry.Feed(msg) +} + +func (m *udpSessionManager) Count() int { + m.mutex.RLock() + defer m.mutex.RUnlock() + return len(m.m) +} diff --git a/v2rayng/hysteria/core/server/udp_test.go b/v2rayng/hysteria/core/server/udp_test.go new file mode 100644 index 0000000000..8aa899f301 --- /dev/null +++ b/v2rayng/hysteria/core/server/udp_test.go @@ -0,0 +1,191 @@ +package server + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "go.uber.org/goleak" + + "github.com/apernet/hysteria/core/v2/internal/protocol" +) + +func TestUDPSessionManager(t *testing.T) { + io := newMockUDPIO(t) + eventLogger := newMockUDPEventLogger(t) + sm := newUDPSessionManager(io, eventLogger, 2*time.Second) + + msgCh := make(chan *protocol.UDPMessage, 4) + io.EXPECT().ReceiveMessage().RunAndReturn(func() (*protocol.UDPMessage, error) { + m := <-msgCh + if m == nil { + return nil, errors.New("closed") + } + return m, nil + }) + + go sm.Run() + + udpReadFunc := func(addr string, ch chan []byte, b []byte) (int, string, error) { + bs := <-ch + if bs == nil { + return 0, "", errors.New("closed") + } + n := copy(b, bs) + return n, addr, nil + } + + // Test normal session creation & timeout + msg1 := &protocol.UDPMessage{ + SessionID: 1234, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: "address1.com:9000", + Data: []byte("hello"), + } + eventLogger.EXPECT().New(msg1.SessionID, msg1.Addr).Return().Once() + udpConn1 := newMockUDPConn(t) + udpConn1Ch := make(chan []byte, 1) + io.EXPECT().Hook(msg1.Data, &msg1.Addr).Return(nil).Once() + io.EXPECT().UDP(msg1.Addr).Return(udpConn1, nil).Once() + udpConn1.EXPECT().WriteTo(msg1.Data, msg1.Addr).Return(5, nil).Once() + udpConn1.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(b []byte) (int, string, error) { + return udpReadFunc(msg1.Addr, udpConn1Ch, b) + }) + io.EXPECT().SendMessage(mock.Anything, &protocol.UDPMessage{ + SessionID: msg1.SessionID, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: msg1.Addr, + Data: []byte("hi back"), + }).Return(nil).Once() + msgCh <- msg1 + udpConn1Ch <- []byte("hi back") + + msg2data := []byte("how are you doing?") + msg2_1 := &protocol.UDPMessage{ + SessionID: 5678, + PacketID: 0, + FragID: 0, + FragCount: 2, + Addr: "address2.net:12450", + Data: msg2data[:6], + } + msg2_2 := &protocol.UDPMessage{ + SessionID: 5678, + PacketID: 0, + FragID: 1, + FragCount: 2, + Addr: "address2.net:12450", + Data: msg2data[6:], + } + + eventLogger.EXPECT().New(msg2_1.SessionID, msg2_1.Addr).Return().Once() + udpConn2 := newMockUDPConn(t) + udpConn2Ch := make(chan []byte, 1) + // On fragmentation, make sure hook gets the whole message + io.EXPECT().Hook(msg2data, &msg2_1.Addr).Return(nil).Once() + io.EXPECT().UDP(msg2_1.Addr).Return(udpConn2, nil).Once() + udpConn2.EXPECT().WriteTo(msg2data, msg2_1.Addr).Return(11, nil).Once() + udpConn2.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(b []byte) (int, string, error) { + return udpReadFunc(msg2_1.Addr, udpConn2Ch, b) + }) + io.EXPECT().SendMessage(mock.Anything, &protocol.UDPMessage{ + SessionID: msg2_1.SessionID, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: msg2_1.Addr, + Data: []byte("im fine"), + }).Return(nil).Once() + msgCh <- msg2_1 + msgCh <- msg2_2 + udpConn2Ch <- []byte("im fine") + + msg3 := &protocol.UDPMessage{ + SessionID: 1234, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: "address1.com:9000", + Data: []byte("who are you?"), + } + udpConn1.EXPECT().WriteTo(msg3.Data, msg3.Addr).Return(12, nil).Once() + io.EXPECT().SendMessage(mock.Anything, &protocol.UDPMessage{ + SessionID: msg3.SessionID, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: msg3.Addr, + Data: []byte("im your father"), + }).Return(nil).Once() + msgCh <- msg3 + udpConn1Ch <- []byte("im your father") + + // Make sure timeout works (connections closed & close events emitted) + udpConn1.EXPECT().Close().RunAndReturn(func() error { + close(udpConn1Ch) + return nil + }).Once() + udpConn2.EXPECT().Close().RunAndReturn(func() error { + close(udpConn2Ch) + return nil + }).Once() + eventLogger.EXPECT().Close(msg1.SessionID, nil).Once() + eventLogger.EXPECT().Close(msg2_1.SessionID, nil).Once() + + time.Sleep(3 * time.Second) // Wait for timeout + mock.AssertExpectationsForObjects(t, io, eventLogger, udpConn1, udpConn2) + + // Test UDP connection close error propagation + errUDPClosed := errors.New("UDP connection closed") + msg4 := &protocol.UDPMessage{ + SessionID: 666, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: "oh-no.com:27015", + Data: []byte("dont say bye"), + } + eventLogger.EXPECT().New(msg4.SessionID, msg4.Addr).Return().Once() + udpConn4 := newMockUDPConn(t) + io.EXPECT().Hook(msg4.Data, &msg4.Addr).Return(nil).Once() + io.EXPECT().UDP(msg4.Addr).Return(udpConn4, nil).Once() + udpConn4.EXPECT().WriteTo(msg4.Data, msg4.Addr).Return(12, nil).Once() + udpConn4.EXPECT().ReadFrom(mock.Anything).Return(0, "", errUDPClosed).Once() + udpConn4.EXPECT().Close().Return(nil).Once() + eventLogger.EXPECT().Close(msg4.SessionID, errUDPClosed).Once() + msgCh <- msg4 + + time.Sleep(1 * time.Second) + mock.AssertExpectationsForObjects(t, io, eventLogger, udpConn4) + + // Test UDP connection creation error propagation + errUDPIO := errors.New("UDP IO error") + msg5 := &protocol.UDPMessage{ + SessionID: 777, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: "callmemaybe.com:15353", + Data: []byte("babe i miss you"), + } + eventLogger.EXPECT().New(msg5.SessionID, msg5.Addr).Return().Once() + io.EXPECT().Hook(msg5.Data, &msg5.Addr).Return(nil).Once() + io.EXPECT().UDP(msg5.Addr).Return(nil, errUDPIO).Once() + eventLogger.EXPECT().Close(msg5.SessionID, errUDPIO).Once() + msgCh <- msg5 + + time.Sleep(1 * time.Second) + mock.AssertExpectationsForObjects(t, io, eventLogger) + + // Leak checks + close(msgCh) // This will return error from ReceiveMessage(), should stop the session manager + time.Sleep(1 * time.Second) // Wait one more second just to be sure + assert.Zero(t, sm.Count(), "session count should be 0") + goleak.VerifyNone(t) +} diff --git a/v2rayng/hysteria/extras/auth/command.go b/v2rayng/hysteria/extras/auth/command.go new file mode 100644 index 0000000000..a981a371b3 --- /dev/null +++ b/v2rayng/hysteria/extras/auth/command.go @@ -0,0 +1,28 @@ +package auth + +import ( + "net" + "os/exec" + "strconv" + "strings" + + "github.com/apernet/hysteria/core/v2/server" +) + +var _ server.Authenticator = &CommandAuthenticator{} + +type CommandAuthenticator struct { + Cmd string +} + +func (a *CommandAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) { + cmd := exec.Command(a.Cmd, addr.String(), auth, strconv.Itoa(int(tx))) + out, err := cmd.Output() + if err != nil { + // This includes failing to execute the command, + // or the command exiting with a non-zero exit code. + return false, "" + } else { + return true, strings.TrimSpace(string(out)) + } +} diff --git a/v2rayng/hysteria/extras/auth/http.go b/v2rayng/hysteria/extras/auth/http.go new file mode 100644 index 0000000000..379a8d7e0d --- /dev/null +++ b/v2rayng/hysteria/extras/auth/http.go @@ -0,0 +1,90 @@ +package auth + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "errors" + "io" + "net" + "net/http" + "time" + + "github.com/apernet/hysteria/core/v2/server" +) + +const ( + httpAuthTimeout = 10 * time.Second +) + +var _ server.Authenticator = &HTTPAuthenticator{} + +var errInvalidStatusCode = errors.New("invalid status code") + +type HTTPAuthenticator struct { + Client *http.Client + URL string +} + +func NewHTTPAuthenticator(url string, insecure bool) *HTTPAuthenticator { + tr := http.DefaultTransport.(*http.Transport).Clone() + tr.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: insecure, + } + return &HTTPAuthenticator{ + Client: &http.Client{ + Transport: tr, + Timeout: httpAuthTimeout, + }, + URL: url, + } +} + +type httpAuthRequest struct { + Addr string `json:"addr"` + Auth string `json:"auth"` + Tx uint64 `json:"tx"` +} + +type httpAuthResponse struct { + OK bool `json:"ok"` + ID string `json:"id"` +} + +func (a *HTTPAuthenticator) post(req *httpAuthRequest) (*httpAuthResponse, error) { + bs, err := json.Marshal(req) + if err != nil { + return nil, err + } + resp, err := a.Client.Post(a.URL, "application/json", bytes.NewReader(bs)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errInvalidStatusCode + } + respData, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + var authResp httpAuthResponse + err = json.Unmarshal(respData, &authResp) + if err != nil { + return nil, err + } + return &authResp, nil +} + +func (a *HTTPAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) { + req := &httpAuthRequest{ + Addr: addr.String(), + Auth: auth, + Tx: tx, + } + resp, err := a.post(req) + if err != nil { + return false, "" + } + return resp.OK, resp.ID +} diff --git a/v2rayng/hysteria/extras/auth/http_test.go b/v2rayng/hysteria/extras/auth/http_test.go new file mode 100644 index 0000000000..243776340e --- /dev/null +++ b/v2rayng/hysteria/extras/auth/http_test.go @@ -0,0 +1,36 @@ +package auth + +import ( + "net" + "os/exec" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestHTTPAuthenticator(t *testing.T) { + // Run the Python test auth server + cmd := exec.Command("python", "http_test.py") + err := cmd.Start() + assert.NoError(t, err) + defer cmd.Process.Kill() + + time.Sleep(1 * time.Second) // Wait for the server to start + + auth := NewHTTPAuthenticator("http://127.0.0.1:5000/auth", false) + + ok, id := auth.Authenticate(&net.UDPAddr{ + IP: net.ParseIP("1.2.3.4"), + Port: 34567, + }, "idk", 123) + assert.False(t, ok) + assert.Equal(t, "", id) + + ok, id = auth.Authenticate(&net.UDPAddr{ + IP: net.ParseIP("123.123.123.123"), + Port: 5566, + }, "wahaha", 12345) + assert.True(t, ok) + assert.Equal(t, "some_unique_id", id) +} diff --git a/v2rayng/hysteria/extras/auth/http_test.py b/v2rayng/hysteria/extras/auth/http_test.py new file mode 100644 index 0000000000..cb6dd0603f --- /dev/null +++ b/v2rayng/hysteria/extras/auth/http_test.py @@ -0,0 +1,24 @@ +from flask import Flask, request, jsonify + +app = Flask(__name__) + + +@app.route("/auth", methods=["POST"]) +def auth(): + data = request.json + + if data is None: + return jsonify({"ok": False, "id": ""}), 400 + + addr = data.get("addr", "") + auth = data.get("auth", "") + tx = data.get("tx", 0) + + if addr == "123.123.123.123:5566" and auth == "wahaha" and tx == 12345: + return jsonify({"ok": True, "id": "some_unique_id"}) + else: + return jsonify({"ok": False, "id": ""}) + + +if __name__ == "__main__": + app.run() diff --git a/v2rayng/hysteria/extras/auth/password.go b/v2rayng/hysteria/extras/auth/password.go new file mode 100644 index 0000000000..c00f399757 --- /dev/null +++ b/v2rayng/hysteria/extras/auth/password.go @@ -0,0 +1,22 @@ +package auth + +import ( + "net" + + "github.com/apernet/hysteria/core/v2/server" +) + +var _ server.Authenticator = &PasswordAuthenticator{} + +// PasswordAuthenticator is a simple authenticator that checks the password against a single string. +type PasswordAuthenticator struct { + Password string +} + +func (a *PasswordAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) { + if auth == a.Password { + return true, "user" + } else { + return false, "" + } +} diff --git a/v2rayng/hysteria/extras/auth/password_test.go b/v2rayng/hysteria/extras/auth/password_test.go new file mode 100644 index 0000000000..9a358b0828 --- /dev/null +++ b/v2rayng/hysteria/extras/auth/password_test.go @@ -0,0 +1,65 @@ +package auth + +import ( + "net" + "testing" +) + +func TestPasswordAuthenticator(t *testing.T) { + type fields struct { + Password string + } + type args struct { + addr net.Addr + auth string + tx uint64 + } + tests := []struct { + name string + fields fields + args args + wantOk bool + wantId string + }{ + { + name: "correct", + fields: fields{ + Password: "yes,yes", + }, + args: args{ + addr: nil, + auth: "yes,yes", + tx: 0, + }, + wantOk: true, + wantId: "user", + }, + { + name: "incorrect", + fields: fields{ + Password: "something_somehow", + }, + args: args{ + addr: nil, + auth: "random", + tx: 0, + }, + wantOk: false, + wantId: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PasswordAuthenticator{ + Password: tt.fields.Password, + } + gotOk, gotId := a.Authenticate(tt.args.addr, tt.args.auth, tt.args.tx) + if gotOk != tt.wantOk { + t.Errorf("Authenticate() gotOk = %v, want %v", gotOk, tt.wantOk) + } + if gotId != tt.wantId { + t.Errorf("Authenticate() gotId = %v, want %v", gotId, tt.wantId) + } + }) + } +} diff --git a/v2rayng/hysteria/extras/auth/userpass.go b/v2rayng/hysteria/extras/auth/userpass.go new file mode 100644 index 0000000000..8faf87aa72 --- /dev/null +++ b/v2rayng/hysteria/extras/auth/userpass.go @@ -0,0 +1,40 @@ +package auth + +import ( + "net" + "strings" + + "github.com/apernet/hysteria/core/v2/server" +) + +const ( + userPassSeparator = ":" +) + +var _ server.Authenticator = &UserPassAuthenticator{} + +// UserPassAuthenticator checks the provided auth string against a map of username/password pairs. +// The format of the auth string must be "username:password". +type UserPassAuthenticator struct { + Users map[string]string +} + +func (a *UserPassAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) { + u, p, ok := splitUserPass(auth) + if !ok { + return false, "" + } + rp, ok := a.Users[u] + if !ok || rp != p { + return false, "" + } + return true, u +} + +func splitUserPass(auth string) (user, pass string, ok bool) { + rs := strings.SplitN(auth, userPassSeparator, 2) + if len(rs) != 2 { + return "", "", false + } + return rs[0], rs[1], true +} diff --git a/v2rayng/hysteria/extras/auth/userpass_test.go b/v2rayng/hysteria/extras/auth/userpass_test.go new file mode 100644 index 0000000000..05f788effc --- /dev/null +++ b/v2rayng/hysteria/extras/auth/userpass_test.go @@ -0,0 +1,103 @@ +package auth + +import ( + "net" + "testing" +) + +func TestUserPassAuthenticator(t *testing.T) { + type fields struct { + Users map[string]string + } + type args struct { + addr net.Addr + auth string + tx uint64 + } + tests := []struct { + name string + fields fields + args args + wantOk bool + wantId string + }{ + { + name: "correct 1", + fields: fields{ + Users: map[string]string{ + "saul": "goodman", + "wang": "123", + }, + }, + args: args{ + addr: nil, + auth: "wang:123", + tx: 0, + }, + wantOk: true, + wantId: "wang", + }, + { + name: "correct 2", + fields: fields{ + Users: map[string]string{ + "gawr": "gura", + "fubuki": "shirakami", + }, + }, + args: args{ + addr: nil, + auth: "gawr:gura", + tx: 0, + }, + wantOk: true, + wantId: "gawr", + }, + { + name: "incorrect 1", + fields: fields{ + Users: map[string]string{ + "gawr": "gura", + "fubuki": "shirakami", + }, + }, + args: args{ + addr: nil, + auth: "random:stranger", + tx: 0, + }, + wantOk: false, + wantId: "", + }, + { + name: "incorrect 2", + fields: fields{ + Users: map[string]string{ + "gawr": "gura", + "fubuki": "shirakami", + }, + }, + args: args{ + addr: nil, + auth: "poop", + tx: 0, + }, + wantOk: false, + wantId: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &UserPassAuthenticator{ + Users: tt.fields.Users, + } + gotOk, gotId := a.Authenticate(tt.args.addr, tt.args.auth, tt.args.tx) + if gotOk != tt.wantOk { + t.Errorf("Authenticate() gotOk = %v, want %v", gotOk, tt.wantOk) + } + if gotId != tt.wantId { + t.Errorf("Authenticate() gotId = %v, want %v", gotId, tt.wantId) + } + }) + } +} diff --git a/v2rayng/hysteria/extras/correctnet/correctnet.go b/v2rayng/hysteria/extras/correctnet/correctnet.go new file mode 100644 index 0000000000..060982597e --- /dev/null +++ b/v2rayng/hysteria/extras/correctnet/correctnet.go @@ -0,0 +1,92 @@ +package correctnet + +import ( + "net" + "net/http" + "strings" +) + +func extractIPFamily(ip net.IP) (family string) { + if len(ip) == 0 { + // real family independent wildcard address, such as ":443" + return "" + } + if p4 := ip.To4(); len(p4) == net.IPv4len { + return "4" + } + return "6" +} + +func tcpAddrNetwork(addr *net.TCPAddr) (network string) { + if addr == nil { + return "tcp" + } + return "tcp" + extractIPFamily(addr.IP) +} + +func udpAddrNetwork(addr *net.UDPAddr) (network string) { + if addr == nil { + return "udp" + } + return "udp" + extractIPFamily(addr.IP) +} + +func ipAddrNetwork(addr *net.IPAddr) (network string) { + if addr == nil { + return "ip" + } + return "ip" + extractIPFamily(addr.IP) +} + +func Listen(network, address string) (net.Listener, error) { + if network == "tcp" { + tcpAddr, err := net.ResolveTCPAddr(network, address) + if err != nil { + return nil, err + } + return ListenTCP(network, tcpAddr) + } + return net.Listen(network, address) +} + +func ListenTCP(network string, laddr *net.TCPAddr) (*net.TCPListener, error) { + if network == "tcp" { + return net.ListenTCP(tcpAddrNetwork(laddr), laddr) + } + return net.ListenTCP(network, laddr) +} + +func ListenPacket(network, address string) (listener net.PacketConn, err error) { + if network == "udp" { + udpAddr, err := net.ResolveUDPAddr(network, address) + if err != nil { + return nil, err + } + return ListenUDP(network, udpAddr) + } + if strings.HasPrefix(network, "ip:") { + proto := network[3:] + ipAddr, err := net.ResolveIPAddr(proto, address) + if err != nil { + return nil, err + } + return net.ListenIP(ipAddrNetwork(ipAddr)+":"+proto, ipAddr) + } + return net.ListenPacket(network, address) +} + +func ListenUDP(network string, laddr *net.UDPAddr) (*net.UDPConn, error) { + if network == "udp" { + return net.ListenUDP(udpAddrNetwork(laddr), laddr) + } + return net.ListenUDP(network, laddr) +} + +func HTTPListenAndServe(address string, handler http.Handler) error { + listener, err := Listen("tcp", address) + if err != nil { + return err + } + defer listener.Close() + return http.Serve(listener, handler) +} diff --git a/v2rayng/hysteria/extras/go.mod b/v2rayng/hysteria/extras/go.mod new file mode 100644 index 0000000000..3da331af60 --- /dev/null +++ b/v2rayng/hysteria/extras/go.mod @@ -0,0 +1,45 @@ +module github.com/apernet/hysteria/extras/v2 + +go 1.22 + +toolchain go1.23.2 + +require ( + github.com/apernet/hysteria/core/v2 v2.0.0-00010101000000-000000000000 + github.com/apernet/quic-go v0.48.2-0.20241104191913-cb103fcecfe7 + github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 + github.com/hashicorp/golang-lru/v2 v2.0.5 + github.com/miekg/dns v1.1.59 + github.com/refraction-networking/utls v1.6.6 + github.com/stretchr/testify v1.9.0 + github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 + golang.org/x/crypto v0.26.0 + golang.org/x/net v0.28.0 + google.golang.org/protobuf v1.34.1 +) + +require ( + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/cloudflare/circl v1.3.9 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/onsi/ginkgo/v2 v2.9.5 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect + go.uber.org/mock v0.4.0 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/apernet/hysteria/core/v2 => ../core diff --git a/v2rayng/hysteria/extras/go.sum b/v2rayng/hysteria/extras/go.sum new file mode 100644 index 0000000000..d07ba7cfce --- /dev/null +++ b/v2rayng/hysteria/extras/go.sum @@ -0,0 +1,122 @@ +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/apernet/quic-go v0.48.2-0.20241104191913-cb103fcecfe7 h1:zO38yBOvQ1dLHbSuaU5BFZ8zalnSDQslj+i/9AGOk9s= +github.com/apernet/quic-go v0.48.2-0.20241104191913-cb103fcecfe7/go.mod h1:LoSUY2chVqNQCDyi4IZGqPpXLy1FuCkE37PKwtJvNGg= +github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0= +github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= +github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= +github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c= +github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= +github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= +github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig= +github.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0= +github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM= +github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 h1:d/Wr/Vl/wiJHc3AHYbYs5I3PucJvRuw3SvbmlIRf+oM= +github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v2rayng/hysteria/extras/masq/server.go b/v2rayng/hysteria/extras/masq/server.go new file mode 100644 index 0000000000..6cd842da0a --- /dev/null +++ b/v2rayng/hysteria/extras/masq/server.go @@ -0,0 +1,94 @@ +package masq + +import ( + "bufio" + "crypto/tls" + "fmt" + "net" + "net/http" + + "github.com/apernet/hysteria/extras/v2/correctnet" +) + +// MasqTCPServer covers the TCP parts of a standard web server (TCP based HTTP/HTTPS). +// We provide this as an option for masquerading, as some may consider a server +// "suspicious" if it only serves the QUIC protocol and not standard HTTP/HTTPS. +type MasqTCPServer struct { + QUICPort int + HTTPSPort int + Handler http.Handler + TLSConfig *tls.Config + ForceHTTPS bool // Always 301 redirect from HTTP to HTTPS +} + +func (s *MasqTCPServer) ListenAndServeHTTP(addr string) error { + return correctnet.HTTPListenAndServe(addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if s.ForceHTTPS { + if s.HTTPSPort == 0 || s.HTTPSPort == 443 { + // Omit port if it's the default + http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusMovedPermanently) + } else { + http.Redirect(w, r, fmt.Sprintf("https://%s:%d%s", r.Host, s.HTTPSPort, r.RequestURI), http.StatusMovedPermanently) + } + return + } + s.Handler.ServeHTTP(newAltSvcHijackResponseWriter(w, s.QUICPort), r) + })) +} + +func (s *MasqTCPServer) ListenAndServeHTTPS(addr string) error { + server := &http.Server{ + Addr: addr, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + s.Handler.ServeHTTP(newAltSvcHijackResponseWriter(w, s.QUICPort), r) + }), + TLSConfig: s.TLSConfig, + } + listener, err := correctnet.Listen("tcp", addr) + if err != nil { + return err + } + defer listener.Close() + return server.ServeTLS(listener, "", "") +} + +var _ http.ResponseWriter = (*altSvcHijackResponseWriter)(nil) + +// altSvcHijackResponseWriter makes sure that the Alt-Svc's port +// is always set with our own value, no matter what the handler sets. +type altSvcHijackResponseWriter struct { + Port int + http.ResponseWriter +} + +func (w *altSvcHijackResponseWriter) WriteHeader(statusCode int) { + w.Header().Set("Alt-Svc", fmt.Sprintf(`h3=":%d"; ma=2592000`, w.Port)) + w.ResponseWriter.WriteHeader(statusCode) +} + +var _ http.Hijacker = (*altSvcHijackResponseWriterHijacker)(nil) + +// altSvcHijackResponseWriterHijacker is a wrapper around altSvcHijackResponseWriter +// that also implements http.Hijacker. This is needed for WebSocket support. +type altSvcHijackResponseWriterHijacker struct { + altSvcHijackResponseWriter +} + +func (w *altSvcHijackResponseWriterHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) { + return w.ResponseWriter.(http.Hijacker).Hijack() +} + +func newAltSvcHijackResponseWriter(w http.ResponseWriter, port int) http.ResponseWriter { + if _, ok := w.(http.Hijacker); ok { + return &altSvcHijackResponseWriterHijacker{ + altSvcHijackResponseWriter: altSvcHijackResponseWriter{ + Port: port, + ResponseWriter: w, + }, + } + } + return &altSvcHijackResponseWriter{ + Port: port, + ResponseWriter: w, + } +} diff --git a/v2rayng/hysteria/extras/obfs/conn.go b/v2rayng/hysteria/extras/obfs/conn.go new file mode 100644 index 0000000000..461319178b --- /dev/null +++ b/v2rayng/hysteria/extras/obfs/conn.go @@ -0,0 +1,121 @@ +package obfs + +import ( + "net" + "sync" + "syscall" + "time" +) + +const udpBufferSize = 2048 // QUIC packets are at most 1500 bytes long, so 2k should be more than enough + +// Obfuscator is the interface that wraps the Obfuscate and Deobfuscate methods. +// Both methods return the number of bytes written to out. +// If a packet is not valid, the methods should return 0. +type Obfuscator interface { + Obfuscate(in, out []byte) int + Deobfuscate(in, out []byte) int +} + +var _ net.PacketConn = (*obfsPacketConn)(nil) + +type obfsPacketConn struct { + Conn net.PacketConn + Obfs Obfuscator + + readBuf []byte + readMutex sync.Mutex + writeBuf []byte + writeMutex sync.Mutex +} + +// obfsPacketConnUDP is a special case of obfsPacketConn that uses a UDPConn +// as the underlying connection. We pass additional methods to quic-go to +// enable UDP-specific optimizations. +type obfsPacketConnUDP struct { + *obfsPacketConn + UDPConn *net.UDPConn +} + +// WrapPacketConn enables obfuscation on a net.PacketConn. +// The obfuscation is transparent to the caller - the n bytes returned by +// ReadFrom and WriteTo are the number of original bytes, not after +// obfuscation/deobfuscation. +func WrapPacketConn(conn net.PacketConn, obfs Obfuscator) net.PacketConn { + opc := &obfsPacketConn{ + Conn: conn, + Obfs: obfs, + readBuf: make([]byte, udpBufferSize), + writeBuf: make([]byte, udpBufferSize), + } + if udpConn, ok := conn.(*net.UDPConn); ok { + return &obfsPacketConnUDP{ + obfsPacketConn: opc, + UDPConn: udpConn, + } + } else { + return opc + } +} + +func (c *obfsPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + for { + c.readMutex.Lock() + n, addr, err = c.Conn.ReadFrom(c.readBuf) + if n <= 0 { + c.readMutex.Unlock() + return + } + n = c.Obfs.Deobfuscate(c.readBuf[:n], p) + c.readMutex.Unlock() + if n > 0 || err != nil { + return + } + // Invalid packet, try again + } +} + +func (c *obfsPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + c.writeMutex.Lock() + nn := c.Obfs.Obfuscate(p, c.writeBuf) + _, err = c.Conn.WriteTo(c.writeBuf[:nn], addr) + c.writeMutex.Unlock() + if err == nil { + n = len(p) + } + return +} + +func (c *obfsPacketConn) Close() error { + return c.Conn.Close() +} + +func (c *obfsPacketConn) LocalAddr() net.Addr { + return c.Conn.LocalAddr() +} + +func (c *obfsPacketConn) SetDeadline(t time.Time) error { + return c.Conn.SetDeadline(t) +} + +func (c *obfsPacketConn) SetReadDeadline(t time.Time) error { + return c.Conn.SetReadDeadline(t) +} + +func (c *obfsPacketConn) SetWriteDeadline(t time.Time) error { + return c.Conn.SetWriteDeadline(t) +} + +// UDP-specific methods below + +func (c *obfsPacketConnUDP) SetReadBuffer(bytes int) error { + return c.UDPConn.SetReadBuffer(bytes) +} + +func (c *obfsPacketConnUDP) SetWriteBuffer(bytes int) error { + return c.UDPConn.SetWriteBuffer(bytes) +} + +func (c *obfsPacketConnUDP) SyscallConn() (syscall.RawConn, error) { + return c.UDPConn.SyscallConn() +} diff --git a/v2rayng/hysteria/extras/obfs/salamander.go b/v2rayng/hysteria/extras/obfs/salamander.go new file mode 100644 index 0000000000..50a3ce2630 --- /dev/null +++ b/v2rayng/hysteria/extras/obfs/salamander.go @@ -0,0 +1,71 @@ +package obfs + +import ( + "fmt" + "math/rand" + "sync" + "time" + + "golang.org/x/crypto/blake2b" +) + +const ( + smPSKMinLen = 4 + smSaltLen = 8 + smKeyLen = blake2b.Size256 +) + +var _ Obfuscator = (*SalamanderObfuscator)(nil) + +var ErrPSKTooShort = fmt.Errorf("PSK must be at least %d bytes", smPSKMinLen) + +// SalamanderObfuscator is an obfuscator that obfuscates each packet with +// the BLAKE2b-256 hash of a pre-shared key combined with a random salt. +// Packet format: [8-byte salt][payload] +type SalamanderObfuscator struct { + PSK []byte + RandSrc *rand.Rand + + lk sync.Mutex +} + +func NewSalamanderObfuscator(psk []byte) (*SalamanderObfuscator, error) { + if len(psk) < smPSKMinLen { + return nil, ErrPSKTooShort + } + return &SalamanderObfuscator{ + PSK: psk, + RandSrc: rand.New(rand.NewSource(time.Now().UnixNano())), + }, nil +} + +func (o *SalamanderObfuscator) Obfuscate(in, out []byte) int { + outLen := len(in) + smSaltLen + if len(out) < outLen { + return 0 + } + o.lk.Lock() + _, _ = o.RandSrc.Read(out[:smSaltLen]) + o.lk.Unlock() + key := o.key(out[:smSaltLen]) + for i, c := range in { + out[i+smSaltLen] = c ^ key[i%smKeyLen] + } + return outLen +} + +func (o *SalamanderObfuscator) Deobfuscate(in, out []byte) int { + outLen := len(in) - smSaltLen + if outLen <= 0 || len(out) < outLen { + return 0 + } + key := o.key(in[:smSaltLen]) + for i, c := range in[smSaltLen:] { + out[i] = c ^ key[i%smKeyLen] + } + return outLen +} + +func (o *SalamanderObfuscator) key(salt []byte) [smKeyLen]byte { + return blake2b.Sum256(append(o.PSK, salt...)) +} diff --git a/v2rayng/hysteria/extras/obfs/salamander_test.go b/v2rayng/hysteria/extras/obfs/salamander_test.go new file mode 100644 index 0000000000..85eafdcce6 --- /dev/null +++ b/v2rayng/hysteria/extras/obfs/salamander_test.go @@ -0,0 +1,45 @@ +package obfs + +import ( + "crypto/rand" + "testing" + + "github.com/stretchr/testify/assert" +) + +func BenchmarkSalamanderObfuscator_Obfuscate(b *testing.B) { + o, _ := NewSalamanderObfuscator([]byte("average_password")) + in := make([]byte, 1200) + _, _ = rand.Read(in) + out := make([]byte, 2048) + b.ResetTimer() + for i := 0; i < b.N; i++ { + o.Obfuscate(in, out) + } +} + +func BenchmarkSalamanderObfuscator_Deobfuscate(b *testing.B) { + o, _ := NewSalamanderObfuscator([]byte("average_password")) + in := make([]byte, 1200) + _, _ = rand.Read(in) + out := make([]byte, 2048) + b.ResetTimer() + for i := 0; i < b.N; i++ { + o.Deobfuscate(in, out) + } +} + +func TestSalamanderObfuscator(t *testing.T) { + o, _ := NewSalamanderObfuscator([]byte("average_password")) + in := make([]byte, 1200) + oOut := make([]byte, 2048) + dOut := make([]byte, 2048) + for i := 0; i < 1000; i++ { + _, _ = rand.Read(in) + n := o.Obfuscate(in, oOut) + assert.Equal(t, len(in)+smSaltLen, n) + n = o.Deobfuscate(oOut[:n], dOut) + assert.Equal(t, len(in), n) + assert.Equal(t, in, dOut[:n]) + } +} diff --git a/v2rayng/hysteria/extras/outbounds/.mockery.yaml b/v2rayng/hysteria/extras/outbounds/.mockery.yaml new file mode 100644 index 0000000000..5f5cc6ebe1 --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/.mockery.yaml @@ -0,0 +1,12 @@ +with-expecter: true +inpackage: true +dir: . +packages: + github.com/apernet/hysteria/extras/v2/outbounds: + interfaces: + PluggableOutbound: + config: + mockname: mockPluggableOutbound + UDPConn: + config: + mockname: mockUDPConn diff --git a/v2rayng/hysteria/extras/outbounds/acl.go b/v2rayng/hysteria/extras/outbounds/acl.go new file mode 100644 index 0000000000..ecdeaaa85c --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/acl.go @@ -0,0 +1,121 @@ +package outbounds + +import ( + "errors" + "net" + "os" + "strings" + + "github.com/apernet/hysteria/extras/v2/outbounds/acl" +) + +const ( + aclCacheSize = 1024 +) + +var errRejected = errors.New("rejected") + +// aclEngine is a PluggableOutbound that dispatches connections to different +// outbounds based on ACL rules. +// There are 3 built-in outbounds: +// - direct: directOutbound, auto mode +// - reject: reject the connection +// - default: first outbound in the list, or if the list is empty, equal to direct +// If the user-defined outbounds contain any of the above names, they will +// override the built-in outbounds. +type aclEngine struct { + RuleSet acl.CompiledRuleSet[PluggableOutbound] + Default PluggableOutbound +} + +type OutboundEntry struct { + Name string + Outbound PluggableOutbound +} + +func NewACLEngineFromString(rules string, outbounds []OutboundEntry, geoLoader acl.GeoLoader) (PluggableOutbound, error) { + trs, err := acl.ParseTextRules(rules) + if err != nil { + return nil, err + } + obMap := outboundsToMap(outbounds) + rs, err := acl.Compile[PluggableOutbound](trs, obMap, aclCacheSize, geoLoader) + if err != nil { + return nil, err + } + return &aclEngine{rs, obMap["default"]}, nil +} + +func NewACLEngineFromFile(filename string, outbounds []OutboundEntry, geoLoader acl.GeoLoader) (PluggableOutbound, error) { + bs, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + return NewACLEngineFromString(string(bs), outbounds, geoLoader) +} + +func outboundsToMap(outbounds []OutboundEntry) map[string]PluggableOutbound { + obMap := make(map[string]PluggableOutbound) + for _, ob := range outbounds { + obMap[strings.ToLower(ob.Name)] = ob.Outbound + } + // Add built-in outbounds if not overridden + if _, ok := obMap["direct"]; !ok { + obMap["direct"] = NewDirectOutboundSimple(DirectOutboundModeAuto) + } + if _, ok := obMap["reject"]; !ok { + obMap["reject"] = &aclRejectOutbound{} + } + if _, ok := obMap["default"]; !ok { + if len(outbounds) > 0 { + obMap["default"] = outbounds[0].Outbound + } else { + obMap["default"] = obMap["direct"] + } + } + return obMap +} + +func (a *aclEngine) handle(reqAddr *AddrEx, proto acl.Protocol) PluggableOutbound { + hostInfo := acl.HostInfo{Name: reqAddr.Host} + if reqAddr.ResolveInfo != nil { + hostInfo.IPv4 = reqAddr.ResolveInfo.IPv4 + hostInfo.IPv6 = reqAddr.ResolveInfo.IPv6 + } + ob, hijackIP := a.RuleSet.Match(hostInfo, proto, reqAddr.Port) + if ob == nil { + // No match, use default outbound + return a.Default + } + if hijackIP != nil { + // We must rewrite both Host & ResolveInfo, + // as some outbounds only care about Host. + reqAddr.Host = hijackIP.String() + if ip4 := hijackIP.To4(); ip4 != nil { + reqAddr.ResolveInfo = &ResolveInfo{IPv4: ip4} + } else { + reqAddr.ResolveInfo = &ResolveInfo{IPv6: hijackIP} + } + } + return ob +} + +func (a *aclEngine) TCP(reqAddr *AddrEx) (net.Conn, error) { + ob := a.handle(reqAddr, acl.ProtocolTCP) + return ob.TCP(reqAddr) +} + +func (a *aclEngine) UDP(reqAddr *AddrEx) (UDPConn, error) { + ob := a.handle(reqAddr, acl.ProtocolUDP) + return ob.UDP(reqAddr) +} + +type aclRejectOutbound struct{} + +func (a *aclRejectOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) { + return nil, errRejected +} + +func (a *aclRejectOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) { + return nil, errRejected +} diff --git a/v2rayng/hysteria/extras/outbounds/acl/compile.go b/v2rayng/hysteria/extras/outbounds/acl/compile.go new file mode 100644 index 0000000000..caee138366 --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/acl/compile.go @@ -0,0 +1,303 @@ +package acl + +import ( + "fmt" + "net" + "strconv" + "strings" + + "github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo" + + lru "github.com/hashicorp/golang-lru/v2" +) + +type Protocol int + +const ( + ProtocolBoth Protocol = iota + ProtocolTCP + ProtocolUDP +) + +type Outbound interface { + any +} + +type HostInfo struct { + Name string + IPv4 net.IP + IPv6 net.IP +} + +func (h HostInfo) String() string { + return fmt.Sprintf("%s|%s|%s", h.Name, h.IPv4, h.IPv6) +} + +type CompiledRuleSet[O Outbound] interface { + Match(host HostInfo, proto Protocol, port uint16) (O, net.IP) +} + +type compiledRule[O Outbound] struct { + Outbound O + HostMatcher hostMatcher + Protocol Protocol + StartPort uint16 + EndPort uint16 + HijackAddress net.IP +} + +func (r *compiledRule[O]) Match(host HostInfo, proto Protocol, port uint16) bool { + if r.Protocol != ProtocolBoth && r.Protocol != proto { + return false + } + if r.StartPort != 0 && (port < r.StartPort || port > r.EndPort) { + return false + } + return r.HostMatcher.Match(host) +} + +type matchResult[O Outbound] struct { + Outbound O + HijackAddress net.IP +} + +type compiledRuleSetImpl[O Outbound] struct { + Rules []compiledRule[O] + Cache *lru.Cache[string, matchResult[O]] // key: HostInfo.String() +} + +func (s *compiledRuleSetImpl[O]) Match(host HostInfo, proto Protocol, port uint16) (O, net.IP) { + host.Name = strings.ToLower(host.Name) // Normalize host name to lower case + key := host.String() + if result, ok := s.Cache.Get(key); ok { + return result.Outbound, result.HijackAddress + } + for _, rule := range s.Rules { + if rule.Match(host, proto, port) { + result := matchResult[O]{rule.Outbound, rule.HijackAddress} + s.Cache.Add(key, result) + return result.Outbound, result.HijackAddress + } + } + // No match should also be cached + var zero O + s.Cache.Add(key, matchResult[O]{zero, nil}) + return zero, nil +} + +type CompilationError struct { + LineNum int + Message string +} + +func (e *CompilationError) Error() string { + return fmt.Sprintf("error at line %d: %s", e.LineNum, e.Message) +} + +type GeoLoader interface { + LoadGeoIP() (map[string]*v2geo.GeoIP, error) + LoadGeoSite() (map[string]*v2geo.GeoSite, error) +} + +// Compile compiles TextRules into a CompiledRuleSet. +// Names in the outbounds map MUST be in all lower case. +// We want on-demand loading of GeoIP/GeoSite databases, so instead of passing the +// databases directly, we use a GeoLoader interface to load them only when needed +// by at least one rule. +func Compile[O Outbound](rules []TextRule, outbounds map[string]O, + cacheSize int, geoLoader GeoLoader, +) (CompiledRuleSet[O], error) { + compiledRules := make([]compiledRule[O], len(rules)) + for i, rule := range rules { + outbound, ok := outbounds[strings.ToLower(rule.Outbound)] + if !ok { + return nil, &CompilationError{rule.LineNum, fmt.Sprintf("outbound %s not found", rule.Outbound)} + } + hm, errStr := compileHostMatcher(rule.Address, geoLoader) + if errStr != "" { + return nil, &CompilationError{rule.LineNum, errStr} + } + proto, startPort, endPort, ok := parseProtoPort(rule.ProtoPort) + if !ok { + return nil, &CompilationError{rule.LineNum, fmt.Sprintf("invalid protocol/port: %s", rule.ProtoPort)} + } + var hijackAddress net.IP + if rule.HijackAddress != "" { + hijackAddress = net.ParseIP(rule.HijackAddress) + if hijackAddress == nil { + return nil, &CompilationError{rule.LineNum, fmt.Sprintf("invalid hijack address (must be an IP address): %s", rule.HijackAddress)} + } + } + compiledRules[i] = compiledRule[O]{outbound, hm, proto, startPort, endPort, hijackAddress} + } + cache, err := lru.New[string, matchResult[O]](cacheSize) + if err != nil { + return nil, err + } + return &compiledRuleSetImpl[O]{compiledRules, cache}, nil +} + +// parseProtoPort parses the protocol and port from a protoPort string. +// protoPort must be in one of the following formats: +// +// proto/port +// proto/* +// proto +// */port +// */* +// * +// [empty] (same as *) +// +// proto must be either "tcp" or "udp", case-insensitive. +func parseProtoPort(protoPort string) (Protocol, uint16, uint16, bool) { + protoPort = strings.ToLower(protoPort) + if protoPort == "" || protoPort == "*" || protoPort == "*/*" { + return ProtocolBoth, 0, 0, true + } + parts := strings.SplitN(protoPort, "/", 2) + if len(parts) == 1 { + // No port, only protocol + switch parts[0] { + case "tcp": + return ProtocolTCP, 0, 0, true + case "udp": + return ProtocolUDP, 0, 0, true + default: + return ProtocolBoth, 0, 0, false + } + } else { + // Both protocol and port + var proto Protocol + var startPort, endPort uint16 + switch parts[0] { + case "tcp": + proto = ProtocolTCP + case "udp": + proto = ProtocolUDP + case "*": + proto = ProtocolBoth + default: + return ProtocolBoth, 0, 0, false + } + if parts[1] != "*" { + // We allow either a single port or a range (e.g. "1000-2000") + ports := strings.SplitN(strings.TrimSpace(parts[1]), "-", 2) + if len(ports) == 1 { + p64, err := strconv.ParseUint(parts[1], 10, 16) + if err != nil { + return ProtocolBoth, 0, 0, false + } + startPort = uint16(p64) + endPort = startPort + } else { + p64, err := strconv.ParseUint(ports[0], 10, 16) + if err != nil { + return ProtocolBoth, 0, 0, false + } + startPort = uint16(p64) + p64, err = strconv.ParseUint(ports[1], 10, 16) + if err != nil { + return ProtocolBoth, 0, 0, false + } + endPort = uint16(p64) + if startPort > endPort { + return ProtocolBoth, 0, 0, false + } + } + } + return proto, startPort, endPort, true + } +} + +func compileHostMatcher(addr string, geoLoader GeoLoader) (hostMatcher, string) { + addr = strings.ToLower(addr) // Normalize to lower case + if addr == "*" || addr == "all" { + // Match all hosts + return &allMatcher{}, "" + } + if strings.HasPrefix(addr, "geoip:") { + // GeoIP matcher + country := addr[6:] + if len(country) == 0 { + return nil, "empty GeoIP country code" + } + gMap, err := geoLoader.LoadGeoIP() + if err != nil { + return nil, err.Error() + } + list, ok := gMap[country] + if !ok || list == nil { + return nil, fmt.Sprintf("GeoIP country code %s not found", country) + } + m, err := newGeoIPMatcher(list) + if err != nil { + return nil, err.Error() + } + return m, "" + } + if strings.HasPrefix(addr, "geosite:") { + // GeoSite matcher + name, attrs := parseGeoSiteName(addr[8:]) + if len(name) == 0 { + return nil, "empty GeoSite name" + } + gMap, err := geoLoader.LoadGeoSite() + if err != nil { + return nil, err.Error() + } + list, ok := gMap[name] + if !ok || list == nil { + return nil, fmt.Sprintf("GeoSite name %s not found", name) + } + m, err := newGeositeMatcher(list, attrs) + if err != nil { + return nil, err.Error() + } + return m, "" + } + if strings.HasPrefix(addr, "suffix:") { + // Domain suffix matcher + suffix := addr[7:] + if len(suffix) == 0 { + return nil, "empty domain suffix" + } + return &domainMatcher{ + Pattern: suffix, + Mode: domainMatchSuffix, + }, "" + } + if strings.Contains(addr, "/") { + // CIDR matcher + _, ipnet, err := net.ParseCIDR(addr) + if err != nil { + return nil, fmt.Sprintf("invalid CIDR address: %s", addr) + } + return &cidrMatcher{ipnet}, "" + } + if ip := net.ParseIP(addr); ip != nil { + // Single IP matcher + return &ipMatcher{ip}, "" + } + if strings.Contains(addr, "*") { + // Wildcard domain matcher + return &domainMatcher{ + Pattern: addr, + Mode: domainMatchWildcard, + }, "" + } + // Nothing else matched, treat it as a non-wildcard domain + return &domainMatcher{ + Pattern: addr, + Mode: domainMatchExact, + }, "" +} + +func parseGeoSiteName(s string) (string, []string) { + parts := strings.Split(s, "@") + base := strings.TrimSpace(parts[0]) + attrs := parts[1:] + for i := range attrs { + attrs[i] = strings.TrimSpace(attrs[i]) + } + return base, attrs +} diff --git a/v2rayng/hysteria/extras/outbounds/acl/compile_test.go b/v2rayng/hysteria/extras/outbounds/acl/compile_test.go new file mode 100644 index 0000000000..bf51f68b7d --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/acl/compile_test.go @@ -0,0 +1,337 @@ +package acl + +import ( + "net" + "testing" + + "github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo" + + "github.com/stretchr/testify/assert" +) + +var _ GeoLoader = (*testGeoLoader)(nil) + +type testGeoLoader struct{} + +func (l *testGeoLoader) LoadGeoIP() (map[string]*v2geo.GeoIP, error) { + return v2geo.LoadGeoIP("v2geo/geoip.dat") +} + +func (l *testGeoLoader) LoadGeoSite() (map[string]*v2geo.GeoSite, error) { + return v2geo.LoadGeoSite("v2geo/geosite.dat") +} + +func TestCompile(t *testing.T) { + ob1, ob2, ob3, ob4, ob5, ob6 := 1, 2, 3, 4, 5, 6 + rules := []TextRule{ + { + Outbound: "ob1", + Address: "1.2.3.4", + ProtoPort: "", + HijackAddress: "", + }, + { + Outbound: "ob2", + Address: "8.8.8.0/24", + ProtoPort: "*", + HijackAddress: "1.1.1.1", + }, + { + Outbound: "ob3", + Address: "all", + ProtoPort: "udp/443", + HijackAddress: "", + }, + { + Outbound: "ob1", + Address: "2606:4700::6810:85e5", + ProtoPort: "tcp", + HijackAddress: "2606:4700::6810:85e6", + }, + { + Outbound: "ob2", + Address: "2606:4700::/44", + ProtoPort: "*/8888", + HijackAddress: "", + }, + { + Outbound: "ob3", + Address: "*.v2ex.com", + ProtoPort: "udp", + HijackAddress: "", + }, + { + Outbound: "ob1", + Address: "crap.v2ex.com", + ProtoPort: "tcp/80", + HijackAddress: "2.2.2.2", + }, + { + Outbound: "ob2", + Address: "geoip:JP", + ProtoPort: "*/*", + HijackAddress: "", + }, + { + Outbound: "ob4", + Address: "geosite:4chan", + ProtoPort: "*/*", + HijackAddress: "", + }, + { + Outbound: "ob4", + Address: "geosite:google @cn", + ProtoPort: "*/*", + HijackAddress: "", + }, + { + Outbound: "ob5", + Address: "suffix:microsoft.com", + ProtoPort: "*/*", + HijackAddress: "", + }, + { + Outbound: "ob6", + Address: "all", + ProtoPort: "tcp/6881-6889", + HijackAddress: "", + }, + } + comp, err := Compile[int](rules, map[string]int{ + "ob1": ob1, + "ob2": ob2, + "ob3": ob3, + "ob4": ob4, + "ob5": ob5, + "ob6": ob6, + }, 100, &testGeoLoader{}) + assert.NoError(t, err) + + tests := []struct { + host HostInfo + proto Protocol + port uint16 + wantOutbound int + wantIP net.IP + }{ + { + host: HostInfo{ + IPv4: net.ParseIP("1.2.3.4"), + }, + proto: ProtocolTCP, + port: 1234, + wantOutbound: ob1, + wantIP: nil, + }, + { + host: HostInfo{ + IPv4: net.ParseIP("8.8.8.4"), + }, + proto: ProtocolUDP, + port: 5353, + wantOutbound: ob2, + wantIP: net.ParseIP("1.1.1.1"), + }, + { + host: HostInfo{ + Name: "lean.delicious.com", + }, + proto: ProtocolUDP, + port: 443, + wantOutbound: ob3, + wantIP: nil, + }, + { + host: HostInfo{ + IPv6: net.ParseIP("2606:4700::6810:85e5"), + }, + proto: ProtocolTCP, + port: 80, + wantOutbound: ob1, + wantIP: net.ParseIP("2606:4700::6810:85e6"), + }, + { + host: HostInfo{ + IPv6: net.ParseIP("2606:4700:0:0:0:0:0:1"), + }, + proto: ProtocolUDP, + port: 8888, + wantOutbound: ob2, + wantIP: nil, + }, + { + host: HostInfo{ + Name: "www.v2ex.com", + }, + proto: ProtocolUDP, + port: 1234, + wantOutbound: ob3, + wantIP: nil, + }, + { + host: HostInfo{ + Name: "crap.v2ex.com", + }, + proto: ProtocolTCP, + port: 80, + wantOutbound: ob1, + wantIP: net.ParseIP("2.2.2.2"), + }, + { + host: HostInfo{ + IPv4: net.ParseIP("210.140.92.187"), + }, + proto: ProtocolTCP, + port: 25, + wantOutbound: ob2, + wantIP: nil, + }, + { + host: HostInfo{ + IPv4: net.ParseIP("175.45.176.73"), + }, + proto: ProtocolTCP, + port: 80, + wantOutbound: 0, // no match default + wantIP: nil, + }, + { + host: HostInfo{ + Name: "boards.4channel.org", + }, + proto: ProtocolTCP, + port: 443, + wantOutbound: ob4, + wantIP: nil, + }, + { + host: HostInfo{ + Name: "gstatic-cn.com", + }, + proto: ProtocolUDP, + port: 9999, + wantOutbound: ob4, + wantIP: nil, + }, + { + host: HostInfo{ + Name: "hoho.waymo.com", + }, + proto: ProtocolUDP, + port: 9999, + wantOutbound: 0, // no match default + wantIP: nil, + }, + { + host: HostInfo{ + Name: "microsoft.com", + }, + proto: ProtocolTCP, + port: 6000, + wantOutbound: ob5, + wantIP: nil, + }, + { + host: HostInfo{ + Name: "real.microsoft.com", + }, + proto: ProtocolUDP, + port: 5353, + wantOutbound: ob5, + wantIP: nil, + }, + { + host: HostInfo{ + Name: "fakemicrosoft.com", + }, + proto: ProtocolTCP, + port: 5000, + wantOutbound: 0, // no match default + wantIP: nil, + }, + { + host: HostInfo{ + IPv4: net.ParseIP("223.1.1.1"), + }, + proto: ProtocolTCP, + port: 6883, + wantOutbound: ob6, // match range port rule 6881-6889 + wantIP: nil, + }, + } + + for _, test := range tests { + gotOutbound, gotIP := comp.Match(test.host, test.proto, test.port) + assert.Equal(t, test.wantOutbound, gotOutbound) + assert.Equal(t, test.wantIP, gotIP) + } + + // Test Invalid Port Range Rule + eb1 := 1 + invalidRules := []TextRule{ + { + Outbound: "eb1", + Address: "1.1.2.0/24", + ProtoPort: "*/3-1", + HijackAddress: "", + }, + } + + _, err = Compile[int](invalidRules, map[string]int{ + "eb1": eb1, + }, 100, &testGeoLoader{}) + assert.Error(t, err) +} + +func Test_parseGeoSiteName(t *testing.T) { + tests := []struct { + name string + s string + want string + want1 []string + }{ + { + name: "no attrs", + s: "pornhub", + want: "pornhub", + want1: []string{}, + }, + { + name: "one attr 1", + s: "xiaomi@cn", + want: "xiaomi", + want1: []string{"cn"}, + }, + { + name: "one attr 2", + s: " google @jp ", + want: "google", + want1: []string{"jp"}, + }, + { + name: "two attrs 1", + s: "netflix@jp@kr", + want: "netflix", + want1: []string{"jp", "kr"}, + }, + { + name: "two attrs 2", + s: "netflix @xixi @haha ", + want: "netflix", + want1: []string{"xixi", "haha"}, + }, + { + name: "empty", + s: "", + want: "", + want1: []string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := parseGeoSiteName(tt.s) + assert.Equalf(t, tt.want, got, "parseGeoSiteName(%v)", tt.s) + assert.Equalf(t, tt.want1, got1, "parseGeoSiteName(%v)", tt.s) + }) + } +} diff --git a/v2rayng/hysteria/extras/outbounds/acl/matchers.go b/v2rayng/hysteria/extras/outbounds/acl/matchers.go new file mode 100644 index 0000000000..a24a56b30a --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/acl/matchers.go @@ -0,0 +1,79 @@ +package acl + +import ( + "net" + "strings" + + "golang.org/x/net/idna" +) + +const ( + domainMatchExact = uint8(iota) + domainMatchWildcard + domainMatchSuffix +) + +type hostMatcher interface { + Match(HostInfo) bool +} + +type ipMatcher struct { + IP net.IP +} + +func (m *ipMatcher) Match(host HostInfo) bool { + return m.IP.Equal(host.IPv4) || m.IP.Equal(host.IPv6) +} + +type cidrMatcher struct { + IPNet *net.IPNet +} + +func (m *cidrMatcher) Match(host HostInfo) bool { + return m.IPNet.Contains(host.IPv4) || m.IPNet.Contains(host.IPv6) +} + +type domainMatcher struct { + Pattern string + Mode uint8 +} + +func (m *domainMatcher) Match(host HostInfo) bool { + name, err := idna.ToUnicode(host.Name) + if err != nil { + name = host.Name + } + switch m.Mode { + case domainMatchExact: + return name == m.Pattern + case domainMatchWildcard: + return deepMatchRune([]rune(name), []rune(m.Pattern)) + case domainMatchSuffix: + return name == m.Pattern || strings.HasSuffix(name, "."+m.Pattern) + default: + return false // Invalid mode + } +} + +func deepMatchRune(str, pattern []rune) bool { + for len(pattern) > 0 { + switch pattern[0] { + default: + if len(str) == 0 || str[0] != pattern[0] { + return false + } + case '*': + return deepMatchRune(str, pattern[1:]) || + (len(str) > 0 && deepMatchRune(str[1:], pattern)) + } + str = str[1:] + pattern = pattern[1:] + } + return len(str) == 0 && len(pattern) == 0 +} + +type allMatcher struct{} + +func (m *allMatcher) Match(host HostInfo) bool { + return true +} diff --git a/v2rayng/hysteria/extras/outbounds/acl/matchers_test.go b/v2rayng/hysteria/extras/outbounds/acl/matchers_test.go new file mode 100644 index 0000000000..4da4249f7f --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/acl/matchers_test.go @@ -0,0 +1,351 @@ +package acl + +import ( + "net" + "testing" +) + +func Test_ipMatcher_Match(t *testing.T) { + tests := []struct { + name string + IP net.IP + host HostInfo + want bool + }{ + { + name: "ipv4 match", + IP: net.IPv4(127, 0, 0, 1), + host: HostInfo{ + IPv4: net.IPv4(127, 0, 0, 1), + IPv6: nil, + }, + want: true, + }, + { + name: "ipv6 match", + IP: net.IPv6loopback, + host: HostInfo{ + IPv4: nil, + IPv6: net.IPv6loopback, + }, + want: true, + }, + { + name: "no match", + IP: net.IPv4(127, 0, 0, 1), + host: HostInfo{ + IPv4: net.IPv4(127, 0, 0, 2), + IPv6: net.IPv6loopback, + }, + want: false, + }, + { + name: "both nil", + IP: net.IPv4(127, 0, 0, 1), + host: HostInfo{ + IPv4: nil, + IPv6: nil, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &ipMatcher{ + IP: tt.IP, + } + if got := m.Match(tt.host); got != tt.want { + t.Errorf("Match() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_cidrMatcher_Match(t *testing.T) { + _, cidr1, _ := net.ParseCIDR("192.168.1.0/24") + _, cidr2, _ := net.ParseCIDR("::1/128") + _, cidr3, _ := net.ParseCIDR("0.0.0.0/0") + _, cidr4, _ := net.ParseCIDR("::/0") + + tests := []struct { + name string + IPNet *net.IPNet + host HostInfo + want bool + }{ + { + name: "ipv4 match", + IPNet: cidr1, + host: HostInfo{ + IPv4: net.ParseIP("192.168.1.100"), + IPv6: net.ParseIP("::1"), + }, + want: true, + }, + { + name: "ipv6 match", + IPNet: cidr2, + host: HostInfo{ + IPv4: net.ParseIP("10.0.0.1"), + IPv6: net.ParseIP("::1"), + }, + want: true, + }, + { + name: "no match", + IPNet: cidr1, + host: HostInfo{ + IPv4: net.ParseIP("10.0.0.1"), + IPv6: net.ParseIP("2001:db8::2:1"), + }, + want: false, + }, + { + name: "ipv4 broad", + IPNet: cidr3, + host: HostInfo{ + IPv4: net.ParseIP("10.0.0.1"), + IPv6: net.ParseIP("::1"), + }, + want: true, + }, + { + name: "ipv6 broad", + IPNet: cidr4, + host: HostInfo{ + IPv4: net.ParseIP("10.0.0.1"), + IPv6: net.ParseIP("2001:db8::2:1"), + }, + want: true, + }, + { + name: "both nil", + IPNet: cidr1, + host: HostInfo{ + IPv4: nil, + IPv6: nil, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &cidrMatcher{ + IPNet: tt.IPNet, + } + if got := m.Match(tt.host); got != tt.want { + t.Errorf("Match() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_domainMatcher_Match(t *testing.T) { + type fields struct { + Pattern string + Mode uint8 + } + tests := []struct { + name string + fields fields + host HostInfo + want bool + }{ + { + name: "non-wildcard match", + fields: fields{ + Pattern: "example.com", + Mode: domainMatchExact, + }, + host: HostInfo{ + Name: "example.com", + }, + want: true, + }, + { + name: "non-wildcard IDN match", + fields: fields{ + Pattern: "政府.中国", + Mode: domainMatchExact, + }, + host: HostInfo{ + Name: "xn--mxtq1m.xn--fiqs8s", + }, + want: true, + }, + { + name: "non-wildcard no match", + fields: fields{ + Pattern: "example.com", + Mode: domainMatchExact, + }, + host: HostInfo{ + Name: "example.org", + }, + want: false, + }, + { + name: "non-wildcard IDN no match", + fields: fields{ + Pattern: "政府.中国", + Mode: domainMatchExact, + }, + host: HostInfo{ + Name: "xn--mxtq1m.xn--yfro4i67o", + }, + want: false, + }, + { + name: "wildcard match 1", + fields: fields{ + Pattern: "*.example.com", + Mode: domainMatchWildcard, + }, + host: HostInfo{ + Name: "www.example.com", + }, + want: true, + }, + { + name: "wildcard match 2", + fields: fields{ + Pattern: "example*.com", + Mode: domainMatchWildcard, + }, + host: HostInfo{ + Name: "example2.com", + }, + want: true, + }, + { + name: "wildcard IDN match 1", + fields: fields{ + Pattern: "战狼*.com", + Mode: domainMatchWildcard, + }, + host: HostInfo{ + Name: "xn--2-x14by21c.com", + }, + want: true, + }, + { + name: "wildcard IDN match 2", + fields: fields{ + Pattern: "*大学*", + Mode: domainMatchWildcard, + }, + host: HostInfo{ + Name: "xn--xkry9kk1bz66a.xn--ses554g", + }, + want: true, + }, + { + name: "wildcard no match", + fields: fields{ + Pattern: "*.example.com", + Mode: domainMatchWildcard, + }, + host: HostInfo{ + Name: "example.com", + }, + want: false, + }, + { + name: "wildcard IDN no match", + fields: fields{ + Pattern: "*呵呵*", + Mode: domainMatchWildcard, + }, + host: HostInfo{ + Name: "xn--6qqt7juua.cn", + }, + want: false, + }, + { + name: "suffix match 1", + fields: fields{ + Pattern: "apple.com", + Mode: domainMatchSuffix, + }, + host: HostInfo{ + Name: "apple.com", + }, + want: true, + }, + { + name: "suffix match 2", + fields: fields{ + Pattern: "apple.com", + Mode: domainMatchSuffix, + }, + host: HostInfo{ + Name: "store.apple.com", + }, + want: true, + }, + { + name: "suffix IDN match 1", + fields: fields{ + Pattern: "中国", + Mode: domainMatchSuffix, + }, + host: HostInfo{ + Name: "中国", + }, + want: true, + }, + { + name: "suffix IDN match 2", + fields: fields{ + Pattern: "中国", + Mode: domainMatchSuffix, + }, + host: HostInfo{ + Name: "天安门.中国", + }, + want: true, + }, + { + name: "suffix no match", + fields: fields{ + Pattern: "news.com", + }, + host: HostInfo{ + Name: "fakenews.com", + }, + want: false, + }, + { + name: "suffix IDN no match", + fields: fields{ + Pattern: "冲浪", + }, + host: HostInfo{ + Name: "666.网上冲浪", + }, + want: false, + }, + { + name: "empty", + fields: fields{ + Pattern: "*.example.com", + Mode: domainMatchWildcard, + }, + host: HostInfo{ + Name: "", + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &domainMatcher{ + Pattern: tt.fields.Pattern, + Mode: tt.fields.Mode, + } + if got := m.Match(tt.host); got != tt.want { + t.Errorf("Match() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/v2rayng/hysteria/extras/outbounds/acl/matchers_v2geo.go b/v2rayng/hysteria/extras/outbounds/acl/matchers_v2geo.go new file mode 100644 index 0000000000..ad79b68f57 --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/acl/matchers_v2geo.go @@ -0,0 +1,213 @@ +package acl + +import ( + "bytes" + "errors" + "net" + "regexp" + "sort" + "strings" + + "github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo" +) + +var _ hostMatcher = (*geoipMatcher)(nil) + +type geoipMatcher struct { + N4 []*net.IPNet // sorted + N6 []*net.IPNet // sorted + Inverse bool +} + +// matchIP tries to match the given IP address with the corresponding IPNets. +// Note that this function does NOT handle the Inverse flag. +func (m *geoipMatcher) matchIP(ip net.IP) bool { + var n []*net.IPNet + if ip4 := ip.To4(); ip4 != nil { + // N4 stores IPv4 addresses in 4-byte form. + // Make sure we use it here too, otherwise bytes.Compare will fail. + ip = ip4 + n = m.N4 + } else { + n = m.N6 + } + left, right := 0, len(n)-1 + for left <= right { + mid := (left + right) / 2 + if n[mid].Contains(ip) { + return true + } else if bytes.Compare(n[mid].IP, ip) < 0 { + left = mid + 1 + } else { + right = mid - 1 + } + } + return false +} + +func (m *geoipMatcher) Match(host HostInfo) bool { + if host.IPv4 != nil { + if m.matchIP(host.IPv4) { + return !m.Inverse + } + } + if host.IPv6 != nil { + if m.matchIP(host.IPv6) { + return !m.Inverse + } + } + return m.Inverse +} + +func newGeoIPMatcher(list *v2geo.GeoIP) (*geoipMatcher, error) { + n4 := make([]*net.IPNet, 0) + n6 := make([]*net.IPNet, 0) + for _, cidr := range list.Cidr { + if len(cidr.Ip) == 4 { + // IPv4 + n4 = append(n4, &net.IPNet{ + IP: cidr.Ip, + Mask: net.CIDRMask(int(cidr.Prefix), 32), + }) + } else if len(cidr.Ip) == 16 { + // IPv6 + n6 = append(n6, &net.IPNet{ + IP: cidr.Ip, + Mask: net.CIDRMask(int(cidr.Prefix), 128), + }) + } else { + return nil, errors.New("invalid IP length") + } + } + // Sort the IPNets, so we can do binary search later. + sort.Slice(n4, func(i, j int) bool { + return bytes.Compare(n4[i].IP, n4[j].IP) < 0 + }) + sort.Slice(n6, func(i, j int) bool { + return bytes.Compare(n6[i].IP, n6[j].IP) < 0 + }) + return &geoipMatcher{ + N4: n4, + N6: n6, + Inverse: list.InverseMatch, + }, nil +} + +var _ hostMatcher = (*geositeMatcher)(nil) + +type geositeDomainType int + +const ( + geositeDomainPlain geositeDomainType = iota + geositeDomainRegex + geositeDomainRoot + geositeDomainFull +) + +type geositeDomain struct { + Type geositeDomainType + Value string + Regex *regexp.Regexp + Attrs map[string]bool +} + +type geositeMatcher struct { + Domains []geositeDomain + // Attributes are matched using "and" logic - if you have multiple attributes here, + // a domain must have all of those attributes to be considered a match. + Attrs []string +} + +func (m *geositeMatcher) matchDomain(domain geositeDomain, host HostInfo) bool { + // Match attributes first + if len(m.Attrs) > 0 { + if len(domain.Attrs) == 0 { + return false + } + for _, attr := range m.Attrs { + if !domain.Attrs[attr] { + return false + } + } + } + + switch domain.Type { + case geositeDomainPlain: + return strings.Contains(host.Name, domain.Value) + case geositeDomainRegex: + if domain.Regex != nil { + return domain.Regex.MatchString(host.Name) + } + case geositeDomainFull: + return host.Name == domain.Value + case geositeDomainRoot: + if host.Name == domain.Value { + return true + } + return strings.HasSuffix(host.Name, "."+domain.Value) + default: + return false + } + return false +} + +func (m *geositeMatcher) Match(host HostInfo) bool { + for _, domain := range m.Domains { + if m.matchDomain(domain, host) { + return true + } + } + return false +} + +func newGeositeMatcher(list *v2geo.GeoSite, attrs []string) (*geositeMatcher, error) { + domains := make([]geositeDomain, len(list.Domain)) + for i, domain := range list.Domain { + switch domain.Type { + case v2geo.Domain_Plain: + domains[i] = geositeDomain{ + Type: geositeDomainPlain, + Value: domain.Value, + Attrs: domainAttributeToMap(domain.Attribute), + } + case v2geo.Domain_Regex: + regex, err := regexp.Compile(domain.Value) + if err != nil { + return nil, err + } + domains[i] = geositeDomain{ + Type: geositeDomainRegex, + Regex: regex, + Attrs: domainAttributeToMap(domain.Attribute), + } + case v2geo.Domain_Full: + domains[i] = geositeDomain{ + Type: geositeDomainFull, + Value: domain.Value, + Attrs: domainAttributeToMap(domain.Attribute), + } + case v2geo.Domain_RootDomain: + domains[i] = geositeDomain{ + Type: geositeDomainRoot, + Value: domain.Value, + Attrs: domainAttributeToMap(domain.Attribute), + } + default: + return nil, errors.New("unsupported domain type") + } + } + return &geositeMatcher{ + Domains: domains, + Attrs: attrs, + }, nil +} + +func domainAttributeToMap(attrs []*v2geo.Domain_Attribute) map[string]bool { + m := make(map[string]bool) + for _, attr := range attrs { + // Supposedly there are also int attributes, + // but nobody seems to use them, so we treat everything as boolean for now. + m[attr.Key] = true + } + return m +} diff --git a/v2rayng/hysteria/extras/outbounds/acl/matchers_v2geo_test.go b/v2rayng/hysteria/extras/outbounds/acl/matchers_v2geo_test.go new file mode 100644 index 0000000000..cc59512ddd --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/acl/matchers_v2geo_test.go @@ -0,0 +1,141 @@ +package acl + +import ( + "net" + "testing" + + "github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo" + "github.com/stretchr/testify/assert" +) + +func Test_geoipMatcher_Match(t *testing.T) { + geoipMap, err := v2geo.LoadGeoIP("v2geo/geoip.dat") + assert.NoError(t, err) + m, err := newGeoIPMatcher(geoipMap["us"]) + assert.NoError(t, err) + + tests := []struct { + name string + host HostInfo + want bool + }{ + { + name: "IPv4 match", + host: HostInfo{ + IPv4: net.ParseIP("73.222.1.100"), + }, + want: true, + }, + { + name: "IPv4 no match", + host: HostInfo{ + IPv4: net.ParseIP("123.123.123.123"), + }, + want: false, + }, + { + name: "IPv6 match", + host: HostInfo{ + IPv6: net.ParseIP("2607:f8b0:4005:80c::2004"), + }, + want: true, + }, + { + name: "IPv6 no match", + host: HostInfo{ + IPv6: net.ParseIP("240e:947:6001::1f8"), + }, + want: false, + }, + { + name: "both nil", + host: HostInfo{ + IPv4: nil, + IPv6: nil, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, m.Match(tt.host), "Match(%v)", tt.host) + }) + } +} + +func Test_geositeMatcher_Match(t *testing.T) { + geositeMap, err := v2geo.LoadGeoSite("v2geo/geosite.dat") + assert.NoError(t, err) + m, err := newGeositeMatcher(geositeMap["apple"], nil) + assert.NoError(t, err) + + tests := []struct { + name string + attrs []string + host HostInfo + want bool + }{ + { + name: "subdomain", + attrs: nil, + host: HostInfo{ + Name: "poop.i-book.com", + }, + want: true, + }, + { + name: "subdomain root", + attrs: nil, + host: HostInfo{ + Name: "applepaycash.net", + }, + want: true, + }, + { + name: "full", + attrs: nil, + host: HostInfo{ + Name: "courier-push-apple.com.akadns.net", + }, + want: true, + }, + { + name: "regexp", + attrs: nil, + host: HostInfo{ + Name: "cdn4.apple-mapkit.com", + }, + want: true, + }, + { + name: "attr match", + attrs: []string{"cn"}, + host: HostInfo{ + Name: "bag.itunes.apple.com", + }, + want: true, + }, + { + name: "attr multi no match", + attrs: []string{"cn", "haha"}, + host: HostInfo{ + Name: "bag.itunes.apple.com", + }, + want: false, + }, + { + name: "attr no match", + attrs: []string{"cn"}, + host: HostInfo{ + Name: "mr-apple.com.tw", + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m.Attrs = tt.attrs + assert.Equalf(t, tt.want, m.Match(tt.host), "Match(%v)", tt.host) + }) + } +} diff --git a/v2rayng/hysteria/extras/outbounds/acl/parse.go b/v2rayng/hysteria/extras/outbounds/acl/parse.go new file mode 100644 index 0000000000..760514d7ff --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/acl/parse.go @@ -0,0 +1,72 @@ +package acl + +import ( + "fmt" + "regexp" + "strings" +) + +var linePattern = regexp.MustCompile(`^(\w+)\s*\(([^,]+)(?:,([^,]+))?(?:,([^,]+))?\)$`) + +type InvalidSyntaxError struct { + Line string + LineNum int +} + +func (e *InvalidSyntaxError) Error() string { + return fmt.Sprintf("invalid syntax at line %d: %s", e.LineNum, e.Line) +} + +// TextRule is the struct representation of a (non-comment) line parsed from an ACL file. +// A line can be parsed into a TextRule as long as it matches one of the following patterns: +// +// outbound(address) +// outbound(address,protoPort) +// outbound(address,protoPort,hijackAddress) +// +// It does not check whether any of the fields is valid - it's up to the compiler to do so. +type TextRule struct { + Outbound string + Address string + ProtoPort string + HijackAddress string + LineNum int +} + +func parseLine(line string, num int) *TextRule { + matches := linePattern.FindStringSubmatch(line) + if matches == nil { + return nil + } + return &TextRule{ + Outbound: matches[1], + Address: strings.TrimSpace(matches[2]), + ProtoPort: strings.TrimSpace(matches[3]), + HijackAddress: strings.TrimSpace(matches[4]), + LineNum: num, + } +} + +func ParseTextRules(text string) ([]TextRule, error) { + rules := make([]TextRule, 0) + lineNum := 0 + for _, line := range strings.Split(text, "\n") { + lineNum++ + // Remove comments + if i := strings.Index(line, "#"); i >= 0 { + line = line[:i] + } + line = strings.TrimSpace(line) + // Skip empty lines + if len(line) == 0 { + continue + } + // Parse line + rule := parseLine(line, lineNum) + if rule == nil { + return nil, &InvalidSyntaxError{line, lineNum} + } + rules = append(rules, *rule) + } + return rules, nil +} diff --git a/v2rayng/hysteria/extras/outbounds/acl/parse_test.go b/v2rayng/hysteria/extras/outbounds/acl/parse_test.go new file mode 100644 index 0000000000..249821cab5 --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/acl/parse_test.go @@ -0,0 +1,70 @@ +package acl + +import ( + "reflect" + "testing" +) + +func TestParseTextRules(t *testing.T) { + tests := []struct { + name string + text string + want []TextRule + wantErr bool + }{ + { + name: "empty", + text: "", + want: []TextRule{}, + wantErr: false, + }, + { + name: "ok", + text: ` +# just a comment + # another comment +direct(1.1.1.1) +direct(8.8.8.0/24) +reject(all, udp/443) # inline comment + reject(geoip:cn) + reject(*.v2ex.com) +my_custom_outbound1(9.9.9.9,*, 8.8.8.8) # bebop +my_custom_outbound2(all) +`, + want: []TextRule{ + {Outbound: "direct", Address: "1.1.1.1", LineNum: 4}, + {Outbound: "direct", Address: "8.8.8.0/24", LineNum: 5}, + {Outbound: "reject", Address: "all", ProtoPort: "udp/443", LineNum: 6}, + {Outbound: "reject", Address: "geoip:cn", LineNum: 7}, + {Outbound: "reject", Address: "*.v2ex.com", LineNum: 8}, + {Outbound: "my_custom_outbound1", Address: "9.9.9.9", ProtoPort: "*", HijackAddress: "8.8.8.8", LineNum: 9}, + {Outbound: "my_custom_outbound2", Address: "all", LineNum: 10}, + }, + wantErr: false, + }, + { + name: "fail 1", + text: `boom()`, + want: nil, + wantErr: true, + }, + { + name: "fail 2", + text: `lol(1,1,1,1)`, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseTextRules(tt.text) + if (err != nil) != tt.wantErr { + t.Errorf("ParseTextRules() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseTextRules() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/v2rayng/hysteria/extras/outbounds/acl/v2geo/load.go b/v2rayng/hysteria/extras/outbounds/acl/v2geo/load.go new file mode 100644 index 0000000000..2dd918c31a --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/acl/v2geo/load.go @@ -0,0 +1,44 @@ +package v2geo + +import ( + "os" + "strings" + + "google.golang.org/protobuf/proto" +) + +// LoadGeoIP loads a GeoIP data file and converts it to a map. +// The keys of the map (country codes) are all normalized to lowercase. +func LoadGeoIP(filename string) (map[string]*GeoIP, error) { + bs, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + var list GeoIPList + if err := proto.Unmarshal(bs, &list); err != nil { + return nil, err + } + m := make(map[string]*GeoIP) + for _, entry := range list.Entry { + m[strings.ToLower(entry.CountryCode)] = entry + } + return m, nil +} + +// LoadGeoSite loads a GeoSite data file and converts it to a map. +// The keys of the map (site keys) are all normalized to lowercase. +func LoadGeoSite(filename string) (map[string]*GeoSite, error) { + bs, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + var list GeoSiteList + if err := proto.Unmarshal(bs, &list); err != nil { + return nil, err + } + m := make(map[string]*GeoSite) + for _, entry := range list.Entry { + m[strings.ToLower(entry.CountryCode)] = entry + } + return m, nil +} diff --git a/v2rayng/hysteria/extras/outbounds/acl/v2geo/load_test.go b/v2rayng/hysteria/extras/outbounds/acl/v2geo/load_test.go new file mode 100644 index 0000000000..e9c901a184 --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/acl/v2geo/load_test.go @@ -0,0 +1,54 @@ +package v2geo + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLoadGeoIP(t *testing.T) { + m, err := LoadGeoIP("geoip.dat") + assert.NoError(t, err) + + // Exact checks since we know the data. + assert.Len(t, m, 252) + assert.Equal(t, m["cn"].CountryCode, "CN") + assert.Len(t, m["cn"].Cidr, 10407) + assert.Equal(t, m["us"].CountryCode, "US") + assert.Len(t, m["us"].Cidr, 193171) + assert.Equal(t, m["private"].CountryCode, "PRIVATE") + assert.Len(t, m["private"].Cidr, 18) + assert.Contains(t, m["private"].Cidr, &CIDR{ + Ip: []byte("\xc0\xa8\x00\x00"), + Prefix: 16, + }) +} + +func TestLoadGeoSite(t *testing.T) { + m, err := LoadGeoSite("geosite.dat") + assert.NoError(t, err) + + // Exact checks since we know the data. + assert.Len(t, m, 1204) + assert.Equal(t, m["netflix"].CountryCode, "NETFLIX") + assert.Len(t, m["netflix"].Domain, 25) + assert.Contains(t, m["netflix"].Domain, &Domain{ + Type: Domain_Full, + Value: "netflix.com.edgesuite.net", + }) + assert.Contains(t, m["netflix"].Domain, &Domain{ + Type: Domain_RootDomain, + Value: "fast.com", + }) + assert.Len(t, m["google"].Domain, 1066) + assert.Contains(t, m["google"].Domain, &Domain{ + Type: Domain_RootDomain, + Value: "ggpht.cn", + Attribute: []*Domain_Attribute{ + { + Key: "cn", + TypedValue: &Domain_Attribute_BoolValue{BoolValue: true}, + }, + }, + }) +} diff --git a/v2rayng/hysteria/extras/outbounds/acl/v2geo/v2geo.pb.go b/v2rayng/hysteria/extras/outbounds/acl/v2geo/v2geo.pb.go new file mode 100644 index 0000000000..bcf2cc7602 --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/acl/v2geo/v2geo.pb.go @@ -0,0 +1,745 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc v4.24.4 +// source: v2geo.proto + +package v2geo + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Type of domain value. +type Domain_Type int32 + +const ( + // The value is used as is. + Domain_Plain Domain_Type = 0 + // The value is used as a regular expression. + Domain_Regex Domain_Type = 1 + // The value is a root domain. + Domain_RootDomain Domain_Type = 2 + // The value is a domain. + Domain_Full Domain_Type = 3 +) + +// Enum value maps for Domain_Type. +var ( + Domain_Type_name = map[int32]string{ + 0: "Plain", + 1: "Regex", + 2: "RootDomain", + 3: "Full", + } + Domain_Type_value = map[string]int32{ + "Plain": 0, + "Regex": 1, + "RootDomain": 2, + "Full": 3, + } +) + +func (x Domain_Type) Enum() *Domain_Type { + p := new(Domain_Type) + *p = x + return p +} + +func (x Domain_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Domain_Type) Descriptor() protoreflect.EnumDescriptor { + return file_v2geo_proto_enumTypes[0].Descriptor() +} + +func (Domain_Type) Type() protoreflect.EnumType { + return &file_v2geo_proto_enumTypes[0] +} + +func (x Domain_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Domain_Type.Descriptor instead. +func (Domain_Type) EnumDescriptor() ([]byte, []int) { + return file_v2geo_proto_rawDescGZIP(), []int{0, 0} +} + +// Domain for routing decision. +type Domain struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Domain matching type. + Type Domain_Type `protobuf:"varint,1,opt,name=type,proto3,enum=Domain_Type" json:"type,omitempty"` + // Domain value. + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + // Attributes of this domain. May be used for filtering. + Attribute []*Domain_Attribute `protobuf:"bytes,3,rep,name=attribute,proto3" json:"attribute,omitempty"` +} + +func (x *Domain) Reset() { + *x = Domain{} + if protoimpl.UnsafeEnabled { + mi := &file_v2geo_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Domain) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Domain) ProtoMessage() {} + +func (x *Domain) ProtoReflect() protoreflect.Message { + mi := &file_v2geo_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Domain.ProtoReflect.Descriptor instead. +func (*Domain) Descriptor() ([]byte, []int) { + return file_v2geo_proto_rawDescGZIP(), []int{0} +} + +func (x *Domain) GetType() Domain_Type { + if x != nil { + return x.Type + } + return Domain_Plain +} + +func (x *Domain) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *Domain) GetAttribute() []*Domain_Attribute { + if x != nil { + return x.Attribute + } + return nil +} + +// IP for routing decision, in CIDR form. +type CIDR struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // IP address, should be either 4 or 16 bytes. + Ip []byte `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"` + // Number of leading ones in the network mask. + Prefix uint32 `protobuf:"varint,2,opt,name=prefix,proto3" json:"prefix,omitempty"` +} + +func (x *CIDR) Reset() { + *x = CIDR{} + if protoimpl.UnsafeEnabled { + mi := &file_v2geo_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CIDR) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CIDR) ProtoMessage() {} + +func (x *CIDR) ProtoReflect() protoreflect.Message { + mi := &file_v2geo_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CIDR.ProtoReflect.Descriptor instead. +func (*CIDR) Descriptor() ([]byte, []int) { + return file_v2geo_proto_rawDescGZIP(), []int{1} +} + +func (x *CIDR) GetIp() []byte { + if x != nil { + return x.Ip + } + return nil +} + +func (x *CIDR) GetPrefix() uint32 { + if x != nil { + return x.Prefix + } + return 0 +} + +type GeoIP struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"` + Cidr []*CIDR `protobuf:"bytes,2,rep,name=cidr,proto3" json:"cidr,omitempty"` + InverseMatch bool `protobuf:"varint,3,opt,name=inverse_match,json=inverseMatch,proto3" json:"inverse_match,omitempty"` + // resource_hash instruct simplified config converter to load domain from geo file. + ResourceHash []byte `protobuf:"bytes,4,opt,name=resource_hash,json=resourceHash,proto3" json:"resource_hash,omitempty"` + Code string `protobuf:"bytes,5,opt,name=code,proto3" json:"code,omitempty"` +} + +func (x *GeoIP) Reset() { + *x = GeoIP{} + if protoimpl.UnsafeEnabled { + mi := &file_v2geo_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GeoIP) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GeoIP) ProtoMessage() {} + +func (x *GeoIP) ProtoReflect() protoreflect.Message { + mi := &file_v2geo_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GeoIP.ProtoReflect.Descriptor instead. +func (*GeoIP) Descriptor() ([]byte, []int) { + return file_v2geo_proto_rawDescGZIP(), []int{2} +} + +func (x *GeoIP) GetCountryCode() string { + if x != nil { + return x.CountryCode + } + return "" +} + +func (x *GeoIP) GetCidr() []*CIDR { + if x != nil { + return x.Cidr + } + return nil +} + +func (x *GeoIP) GetInverseMatch() bool { + if x != nil { + return x.InverseMatch + } + return false +} + +func (x *GeoIP) GetResourceHash() []byte { + if x != nil { + return x.ResourceHash + } + return nil +} + +func (x *GeoIP) GetCode() string { + if x != nil { + return x.Code + } + return "" +} + +type GeoIPList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Entry []*GeoIP `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"` +} + +func (x *GeoIPList) Reset() { + *x = GeoIPList{} + if protoimpl.UnsafeEnabled { + mi := &file_v2geo_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GeoIPList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GeoIPList) ProtoMessage() {} + +func (x *GeoIPList) ProtoReflect() protoreflect.Message { + mi := &file_v2geo_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GeoIPList.ProtoReflect.Descriptor instead. +func (*GeoIPList) Descriptor() ([]byte, []int) { + return file_v2geo_proto_rawDescGZIP(), []int{3} +} + +func (x *GeoIPList) GetEntry() []*GeoIP { + if x != nil { + return x.Entry + } + return nil +} + +type GeoSite struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"` + Domain []*Domain `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"` + // resource_hash instruct simplified config converter to load domain from geo file. + ResourceHash []byte `protobuf:"bytes,3,opt,name=resource_hash,json=resourceHash,proto3" json:"resource_hash,omitempty"` + Code string `protobuf:"bytes,4,opt,name=code,proto3" json:"code,omitempty"` +} + +func (x *GeoSite) Reset() { + *x = GeoSite{} + if protoimpl.UnsafeEnabled { + mi := &file_v2geo_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GeoSite) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GeoSite) ProtoMessage() {} + +func (x *GeoSite) ProtoReflect() protoreflect.Message { + mi := &file_v2geo_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GeoSite.ProtoReflect.Descriptor instead. +func (*GeoSite) Descriptor() ([]byte, []int) { + return file_v2geo_proto_rawDescGZIP(), []int{4} +} + +func (x *GeoSite) GetCountryCode() string { + if x != nil { + return x.CountryCode + } + return "" +} + +func (x *GeoSite) GetDomain() []*Domain { + if x != nil { + return x.Domain + } + return nil +} + +func (x *GeoSite) GetResourceHash() []byte { + if x != nil { + return x.ResourceHash + } + return nil +} + +func (x *GeoSite) GetCode() string { + if x != nil { + return x.Code + } + return "" +} + +type GeoSiteList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Entry []*GeoSite `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"` +} + +func (x *GeoSiteList) Reset() { + *x = GeoSiteList{} + if protoimpl.UnsafeEnabled { + mi := &file_v2geo_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GeoSiteList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GeoSiteList) ProtoMessage() {} + +func (x *GeoSiteList) ProtoReflect() protoreflect.Message { + mi := &file_v2geo_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GeoSiteList.ProtoReflect.Descriptor instead. +func (*GeoSiteList) Descriptor() ([]byte, []int) { + return file_v2geo_proto_rawDescGZIP(), []int{5} +} + +func (x *GeoSiteList) GetEntry() []*GeoSite { + if x != nil { + return x.Entry + } + return nil +} + +type Domain_Attribute struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + // Types that are assignable to TypedValue: + // + // *Domain_Attribute_BoolValue + // *Domain_Attribute_IntValue + TypedValue isDomain_Attribute_TypedValue `protobuf_oneof:"typed_value"` +} + +func (x *Domain_Attribute) Reset() { + *x = Domain_Attribute{} + if protoimpl.UnsafeEnabled { + mi := &file_v2geo_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Domain_Attribute) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Domain_Attribute) ProtoMessage() {} + +func (x *Domain_Attribute) ProtoReflect() protoreflect.Message { + mi := &file_v2geo_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Domain_Attribute.ProtoReflect.Descriptor instead. +func (*Domain_Attribute) Descriptor() ([]byte, []int) { + return file_v2geo_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *Domain_Attribute) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (m *Domain_Attribute) GetTypedValue() isDomain_Attribute_TypedValue { + if m != nil { + return m.TypedValue + } + return nil +} + +func (x *Domain_Attribute) GetBoolValue() bool { + if x, ok := x.GetTypedValue().(*Domain_Attribute_BoolValue); ok { + return x.BoolValue + } + return false +} + +func (x *Domain_Attribute) GetIntValue() int64 { + if x, ok := x.GetTypedValue().(*Domain_Attribute_IntValue); ok { + return x.IntValue + } + return 0 +} + +type isDomain_Attribute_TypedValue interface { + isDomain_Attribute_TypedValue() +} + +type Domain_Attribute_BoolValue struct { + BoolValue bool `protobuf:"varint,2,opt,name=bool_value,json=boolValue,proto3,oneof"` +} + +type Domain_Attribute_IntValue struct { + IntValue int64 `protobuf:"varint,3,opt,name=int_value,json=intValue,proto3,oneof"` +} + +func (*Domain_Attribute_BoolValue) isDomain_Attribute_TypedValue() {} + +func (*Domain_Attribute_IntValue) isDomain_Attribute_TypedValue() {} + +var File_v2geo_proto protoreflect.FileDescriptor + +var file_v2geo_proto_rawDesc = []byte{ + 0x0a, 0x0b, 0x76, 0x32, 0x67, 0x65, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x97, 0x02, + 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x20, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0c, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e, + 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x12, 0x2f, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x1a, 0x6c, 0x0a, 0x09, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x1f, 0x0a, 0x0a, 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x12, 0x1d, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x42, 0x0d, 0x0a, 0x0b, 0x74, 0x79, 0x70, 0x65, 0x64, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, + 0x36, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x6c, 0x61, 0x69, 0x6e, + 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x65, 0x67, 0x65, 0x78, 0x10, 0x01, 0x12, 0x0e, 0x0a, + 0x0a, 0x52, 0x6f, 0x6f, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x10, 0x02, 0x12, 0x08, 0x0a, + 0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0x03, 0x22, 0x2e, 0x0a, 0x04, 0x43, 0x49, 0x44, 0x52, 0x12, + 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x12, + 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x22, 0xa3, 0x01, 0x0a, 0x05, 0x47, 0x65, 0x6f, 0x49, + 0x50, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, + 0x43, 0x6f, 0x64, 0x65, 0x12, 0x19, 0x0a, 0x04, 0x63, 0x69, 0x64, 0x72, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x05, 0x2e, 0x43, 0x49, 0x44, 0x52, 0x52, 0x04, 0x63, 0x69, 0x64, 0x72, 0x12, + 0x23, 0x0a, 0x0d, 0x69, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x65, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x69, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x65, 0x4d, + 0x61, 0x74, 0x63, 0x68, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, + 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x29, 0x0a, + 0x09, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x05, 0x65, 0x6e, + 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x06, 0x2e, 0x47, 0x65, 0x6f, 0x49, + 0x50, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x86, 0x01, 0x0a, 0x07, 0x47, 0x65, 0x6f, + 0x53, 0x69, 0x74, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, + 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, + 0x6e, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, + 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, + 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, + 0x65, 0x22, 0x2d, 0x0a, 0x0b, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74, + 0x12, 0x1e, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x08, 0x2e, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, + 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x76, 0x32, 0x67, 0x65, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, +} + +var ( + file_v2geo_proto_rawDescOnce sync.Once + file_v2geo_proto_rawDescData = file_v2geo_proto_rawDesc +) + +func file_v2geo_proto_rawDescGZIP() []byte { + file_v2geo_proto_rawDescOnce.Do(func() { + file_v2geo_proto_rawDescData = protoimpl.X.CompressGZIP(file_v2geo_proto_rawDescData) + }) + return file_v2geo_proto_rawDescData +} + +var file_v2geo_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_v2geo_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_v2geo_proto_goTypes = []interface{}{ + (Domain_Type)(0), // 0: Domain.Type + (*Domain)(nil), // 1: Domain + (*CIDR)(nil), // 2: CIDR + (*GeoIP)(nil), // 3: GeoIP + (*GeoIPList)(nil), // 4: GeoIPList + (*GeoSite)(nil), // 5: GeoSite + (*GeoSiteList)(nil), // 6: GeoSiteList + (*Domain_Attribute)(nil), // 7: Domain.Attribute +} +var file_v2geo_proto_depIdxs = []int32{ + 0, // 0: Domain.type:type_name -> Domain.Type + 7, // 1: Domain.attribute:type_name -> Domain.Attribute + 2, // 2: GeoIP.cidr:type_name -> CIDR + 3, // 3: GeoIPList.entry:type_name -> GeoIP + 1, // 4: GeoSite.domain:type_name -> Domain + 5, // 5: GeoSiteList.entry:type_name -> GeoSite + 6, // [6:6] is the sub-list for method output_type + 6, // [6:6] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_v2geo_proto_init() } +func file_v2geo_proto_init() { + if File_v2geo_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_v2geo_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Domain); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_v2geo_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CIDR); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_v2geo_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GeoIP); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_v2geo_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GeoIPList); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_v2geo_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GeoSite); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_v2geo_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GeoSiteList); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_v2geo_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Domain_Attribute); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_v2geo_proto_msgTypes[6].OneofWrappers = []interface{}{ + (*Domain_Attribute_BoolValue)(nil), + (*Domain_Attribute_IntValue)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_v2geo_proto_rawDesc, + NumEnums: 1, + NumMessages: 7, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_v2geo_proto_goTypes, + DependencyIndexes: file_v2geo_proto_depIdxs, + EnumInfos: file_v2geo_proto_enumTypes, + MessageInfos: file_v2geo_proto_msgTypes, + }.Build() + File_v2geo_proto = out.File + file_v2geo_proto_rawDesc = nil + file_v2geo_proto_goTypes = nil + file_v2geo_proto_depIdxs = nil +} diff --git a/v2rayng/hysteria/extras/outbounds/acl/v2geo/v2geo.proto b/v2rayng/hysteria/extras/outbounds/acl/v2geo/v2geo.proto new file mode 100644 index 0000000000..48ff6f05c0 --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/acl/v2geo/v2geo.proto @@ -0,0 +1,76 @@ +syntax = "proto3"; + +option go_package = "./v2geo"; + +// This file is copied from +// https://github.com/v2fly/v2ray-core/blob/master/app/router/routercommon/common.proto +// with some modifications. + +// Domain for routing decision. +message Domain { + // Type of domain value. + enum Type { + // The value is used as is. + Plain = 0; + // The value is used as a regular expression. + Regex = 1; + // The value is a root domain. + RootDomain = 2; + // The value is a domain. + Full = 3; + } + + // Domain matching type. + Type type = 1; + + // Domain value. + string value = 2; + + message Attribute { + string key = 1; + + oneof typed_value { + bool bool_value = 2; + int64 int_value = 3; + } + } + + // Attributes of this domain. May be used for filtering. + repeated Attribute attribute = 3; +} + +// IP for routing decision, in CIDR form. +message CIDR { + // IP address, should be either 4 or 16 bytes. + bytes ip = 1; + + // Number of leading ones in the network mask. + uint32 prefix = 2; +} + +message GeoIP { + string country_code = 1; + repeated CIDR cidr = 2; + bool inverse_match = 3; + + // resource_hash instruct simplified config converter to load domain from geo file. + bytes resource_hash = 4; + string code = 5; +} + +message GeoIPList { + repeated GeoIP entry = 1; +} + +message GeoSite { + string country_code = 1; + repeated Domain domain = 2; + + // resource_hash instruct simplified config converter to load domain from geo file. + bytes resource_hash = 3; + string code = 4; +} + +message GeoSiteList { + repeated GeoSite entry = 1; +} diff --git a/v2rayng/hysteria/extras/outbounds/acl_test.go b/v2rayng/hysteria/extras/outbounds/acl_test.go new file mode 100644 index 0000000000..9c68890a0e --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/acl_test.go @@ -0,0 +1,61 @@ +package outbounds + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestACLEngine(t *testing.T) { + ob1, ob2, ob3 := &mockPluggableOutbound{}, &mockPluggableOutbound{}, &mockPluggableOutbound{} + obs := []OutboundEntry{ + {"ob1", ob1}, + {"ob2", ob2}, + {"ob3", ob3}, + {"direct", ob2}, + } + acl, err := NewACLEngineFromString(` +ob2(google.com,tcp) +ob3(youtube.com,udp) +ob1 (1.1.1.1/24,*,8.8.8.8) +Direct(cia.gov) +reJect(nsa.gov) +`, obs, nil) + assert.NoError(t, err) + + // No match, default, should be the first (ob1) + ob1.EXPECT().TCP(&AddrEx{Host: "example.com"}).Return(nil, nil).Once() + conn, err := acl.TCP(&AddrEx{Host: "example.com"}) + assert.NoError(t, err) + assert.Nil(t, conn) + + // Match ob2 + ob2.EXPECT().TCP(&AddrEx{Host: "google.com"}).Return(nil, nil).Once() + conn, err = acl.TCP(&AddrEx{Host: "google.com"}) + assert.NoError(t, err) + assert.Nil(t, conn) + + // Match ob3 + ob3.EXPECT().UDP(&AddrEx{Host: "youtube.com"}).Return(nil, nil).Once() + udpConn, err := acl.UDP(&AddrEx{Host: "youtube.com"}) + assert.NoError(t, err) + assert.Nil(t, udpConn) + + // Match ob1 hijack IP + ob1.EXPECT().TCP(&AddrEx{Host: "8.8.8.8", ResolveInfo: &ResolveInfo{IPv4: net.ParseIP("8.8.8.8").To4()}}).Return(nil, nil).Once() + conn, err = acl.TCP(&AddrEx{ResolveInfo: &ResolveInfo{IPv4: net.ParseIP("1.1.1.22")}}) + assert.NoError(t, err) + assert.Nil(t, conn) + + // direct should be ob2 as we override it + ob2.EXPECT().TCP(&AddrEx{Host: "cia.gov"}).Return(nil, nil).Once() + conn, err = acl.TCP(&AddrEx{Host: "cia.gov"}) + assert.NoError(t, err) + assert.Nil(t, conn) + + // reject + conn, err = acl.TCP(&AddrEx{Host: "nsa.gov"}) + assert.Error(t, err) + assert.Nil(t, conn) +} diff --git a/v2rayng/hysteria/extras/outbounds/dns_https.go b/v2rayng/hysteria/extras/outbounds/dns_https.go new file mode 100644 index 0000000000..daf0dfeb14 --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/dns_https.go @@ -0,0 +1,84 @@ +package outbounds + +import ( + "crypto/tls" + "net" + "net/http" + "time" + + "github.com/babolivier/go-doh-client" +) + +// dohResolver is a PluggableOutbound DNS resolver that resolves hostnames +// using the user-provided DNS-over-HTTPS server. +type dohResolver struct { + Resolver *doh.Resolver + Next PluggableOutbound +} + +func NewDoHResolver(host string, timeout time.Duration, sni string, insecure bool, next PluggableOutbound) PluggableOutbound { + tr := http.DefaultTransport.(*http.Transport).Clone() + tr.TLSClientConfig = &tls.Config{ + ServerName: sni, + InsecureSkipVerify: insecure, + } + return &dohResolver{ + Resolver: &doh.Resolver{ + Host: host, + Class: doh.IN, + HTTPClient: &http.Client{ + Transport: tr, + Timeout: timeoutOrDefault(timeout), + }, + }, + Next: next, + } +} + +func (r *dohResolver) resolve(reqAddr *AddrEx) { + if tryParseIP(reqAddr) { + // The host is already an IP address, we don't need to resolve it. + return + } + type lookupResult struct { + ip net.IP + err error + } + ch4, ch6 := make(chan lookupResult, 1), make(chan lookupResult, 1) + go func() { + recs, _, err := r.Resolver.LookupA(reqAddr.Host) + var ip net.IP + if err == nil && len(recs) > 0 { + ip = net.ParseIP(recs[0].IP4).To4() + } + ch4 <- lookupResult{ip, err} + }() + go func() { + recs, _, err := r.Resolver.LookupAAAA(reqAddr.Host) + var ip net.IP + if err == nil && len(recs) > 0 { + ip = net.ParseIP(recs[0].IP6).To16() + } + ch6 <- lookupResult{ip, err} + }() + result4, result6 := <-ch4, <-ch6 + reqAddr.ResolveInfo = &ResolveInfo{ + IPv4: result4.ip, + IPv6: result6.ip, + } + if result4.err != nil { + reqAddr.ResolveInfo.Err = result4.err + } else if result6.err != nil { + reqAddr.ResolveInfo.Err = result6.err + } +} + +func (r *dohResolver) TCP(reqAddr *AddrEx) (net.Conn, error) { + r.resolve(reqAddr) + return r.Next.TCP(reqAddr) +} + +func (r *dohResolver) UDP(reqAddr *AddrEx) (UDPConn, error) { + r.resolve(reqAddr) + return r.Next.UDP(reqAddr) +} diff --git a/v2rayng/hysteria/extras/outbounds/dns_standard.go b/v2rayng/hysteria/extras/outbounds/dns_standard.go new file mode 100644 index 0000000000..a9df2380cf --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/dns_standard.go @@ -0,0 +1,221 @@ +package outbounds + +import ( + "crypto/tls" + "net" + "time" + + "github.com/miekg/dns" +) + +const ( + resolverDefaultTimeout = 2 * time.Second + standardResolverRetryTimes = 2 +) + +// standardResolver is a PluggableOutbound DNS resolver that resolves hostnames +// using the user-provided DNS server. +// Based on "github.com/miekg/dns", it supports UDP, TCP & DNS-over-TLS (TCP). +type standardResolver struct { + Addr string + Client *dns.Client + Next PluggableOutbound +} + +func NewStandardResolverUDP(addr string, timeout time.Duration, next PluggableOutbound) PluggableOutbound { + return &standardResolver{ + Addr: addDefaultPort(addr), + Client: &dns.Client{ + Timeout: timeoutOrDefault(timeout), + }, + Next: next, + } +} + +func NewStandardResolverTCP(addr string, timeout time.Duration, next PluggableOutbound) PluggableOutbound { + return &standardResolver{ + Addr: addDefaultPort(addr), + Client: &dns.Client{ + Net: "tcp", + Timeout: timeoutOrDefault(timeout), + }, + Next: next, + } +} + +func NewStandardResolverTLS(addr string, timeout time.Duration, sni string, insecure bool, next PluggableOutbound) PluggableOutbound { + return &standardResolver{ + Addr: addDefaultPortTLS(addr), + Client: &dns.Client{ + Net: "tcp-tls", + Timeout: timeoutOrDefault(timeout), + TLSConfig: &tls.Config{ + ServerName: sni, + InsecureSkipVerify: insecure, + }, + }, + Next: next, + } +} + +// addDefaultPort adds the default DNS port (53) to the address if not present. +func addDefaultPort(addr string) string { + if _, _, err := net.SplitHostPort(addr); err != nil { + return net.JoinHostPort(addr, "53") + } + return addr +} + +// addDefaultPortTLS adds the default DNS-over-TLS port (853) to the address if not present. +func addDefaultPortTLS(addr string) string { + if _, _, err := net.SplitHostPort(addr); err != nil { + return net.JoinHostPort(addr, "853") + } + return addr +} + +func timeoutOrDefault(timeout time.Duration) time.Duration { + if timeout == 0 { + return resolverDefaultTimeout + } + return timeout +} + +// skipCNAMEChain skips the CNAME chain and returns the last CNAME target. +// Sometimes the DNS server returns a CNAME chain like this, in one packet: +// domain1.com. CNAME domain2.com. +// domain2.com. CNAME domain3.com. +// In this case, we should avoid sending a query for domain2.com and go +// straight to domain3.com. +func (r *standardResolver) skipCNAMEChain(answers []dns.RR) string { + var lastCNAME string + for _, a := range answers { + if cname, ok := a.(*dns.CNAME); ok { + if lastCNAME == "" { + // First CNAME + lastCNAME = cname.Target + } else if cname.Hdr.Name == lastCNAME { + // CNAME chain + lastCNAME = cname.Target + } else { + // CNAME chain ends + return lastCNAME + } + } + } + return lastCNAME +} + +// lookup4 resolves a hostname to an IPv4 address. +// If there's no IPv4 address, it returns (nil, nil), no error. +func (r *standardResolver) lookup4(host string) (net.IP, error) { + m := new(dns.Msg) + m.SetQuestion(dns.Fqdn(host), dns.TypeA) + m.RecursionDesired = true + resp, _, err := r.Client.Exchange(m, r.Addr) + if err != nil { + return nil, err + } + if len(resp.Answer) == 0 { + return nil, nil + } + // Sometimes the DNS server returns both CNAME and A records in one packet. + hasCNAME := false + for _, a := range resp.Answer { + if aa, ok := a.(*dns.A); ok { + return aa.A.To4(), nil + } else if _, ok := a.(*dns.CNAME); ok { + hasCNAME = true + } + } + if hasCNAME { + return r.lookup4(r.skipCNAMEChain(resp.Answer)) + } else { + // Should not happen + return nil, nil + } +} + +// lookup6 resolves a hostname to an IPv6 address. +// If there's no IPv6 address, it returns (nil, nil), no error. +func (r *standardResolver) lookup6(host string) (net.IP, error) { + m := new(dns.Msg) + m.SetQuestion(dns.Fqdn(host), dns.TypeAAAA) + m.RecursionDesired = true + resp, _, err := r.Client.Exchange(m, r.Addr) + if err != nil { + return nil, err + } + if len(resp.Answer) == 0 { + return nil, nil + } + // Sometimes the DNS server returns both CNAME and AAAA records in one packet. + hasCNAME := false + for _, a := range resp.Answer { + if aa, ok := a.(*dns.AAAA); ok { + return aa.AAAA.To16(), nil + } else if _, ok := a.(*dns.CNAME); ok { + hasCNAME = true + } + } + if hasCNAME { + return r.lookup6(r.skipCNAMEChain(resp.Answer)) + } else { + // Should not happen + return nil, nil + } +} + +func (r *standardResolver) resolve(reqAddr *AddrEx) { + if tryParseIP(reqAddr) { + // The host is already an IP address, we don't need to resolve it. + return + } + type lookupResult struct { + ip net.IP + err error + } + ch4, ch6 := make(chan lookupResult, 1), make(chan lookupResult, 1) + go func() { + var ip net.IP + var err error + for i := 0; i < standardResolverRetryTimes; i++ { + ip, err = r.lookup4(reqAddr.Host) + if err == nil { + break + } + } + ch4 <- lookupResult{ip, err} + }() + go func() { + var ip net.IP + var err error + for i := 0; i < standardResolverRetryTimes; i++ { + ip, err = r.lookup6(reqAddr.Host) + if err == nil { + break + } + } + ch6 <- lookupResult{ip, err} + }() + result4, result6 := <-ch4, <-ch6 + reqAddr.ResolveInfo = &ResolveInfo{ + IPv4: result4.ip, + IPv6: result6.ip, + } + if result4.err != nil { + reqAddr.ResolveInfo.Err = result4.err + } else if result6.err != nil { + reqAddr.ResolveInfo.Err = result6.err + } +} + +func (r *standardResolver) TCP(reqAddr *AddrEx) (net.Conn, error) { + r.resolve(reqAddr) + return r.Next.TCP(reqAddr) +} + +func (r *standardResolver) UDP(reqAddr *AddrEx) (UDPConn, error) { + r.resolve(reqAddr) + return r.Next.UDP(reqAddr) +} diff --git a/v2rayng/hysteria/extras/outbounds/dns_system.go b/v2rayng/hysteria/extras/outbounds/dns_system.go new file mode 100644 index 0000000000..8f9a4292d7 --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/dns_system.go @@ -0,0 +1,41 @@ +package outbounds + +import ( + "net" +) + +// systemResolver is a PluggableOutbound DNS resolver that resolves hostnames +// using the default system DNS server. +// Outbounds typically don't require a resolver, as they can do DNS resolution +// themselves. However, when using ACL, it's necessary to place a resolver in +// front of it in the pipeline (for IP rules to work on domain requests). +type systemResolver struct { + Next PluggableOutbound +} + +func NewSystemResolver(next PluggableOutbound) PluggableOutbound { + return &systemResolver{ + Next: next, + } +} + +func (r *systemResolver) resolve(reqAddr *AddrEx) { + ips, err := net.LookupIP(reqAddr.Host) + if err != nil { + reqAddr.ResolveInfo = &ResolveInfo{Err: err} + return + } + info := &ResolveInfo{} + info.IPv4, info.IPv6 = splitIPv4IPv6(ips) + reqAddr.ResolveInfo = info +} + +func (r *systemResolver) TCP(reqAddr *AddrEx) (net.Conn, error) { + r.resolve(reqAddr) + return r.Next.TCP(reqAddr) +} + +func (r *systemResolver) UDP(reqAddr *AddrEx) (UDPConn, error) { + r.resolve(reqAddr) + return r.Next.UDP(reqAddr) +} diff --git a/v2rayng/hysteria/extras/outbounds/interface.go b/v2rayng/hysteria/extras/outbounds/interface.go new file mode 100644 index 0000000000..1870290b85 --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/interface.go @@ -0,0 +1,133 @@ +package outbounds + +import ( + "net" + "strconv" + + "github.com/apernet/hysteria/core/v2/server" +) + +// The PluggableOutbound system is designed to function in a chain-like manner. +// Not every outbound is an actual outbound; some are just wrappers around other +// outbounds, such as custom resolvers, ACL engine, etc. It is a pipeline where +// each stage can check (and optionally modify) the request before passing it +// on to the next stage. The last stage in the pipeline is always a real outbound +// that actually implements the logic of connecting to the remote server. +// There can also be instances of branching, where requests can be sent to +// different outbound sub-pipelines based on some criteria. + +// PluggableOutbound differs from the built-in Outbound interface from Hysteria core +// in that it uses an AddrEx struct for addresses instead of a string. Because of this +// difference, we need a special PluggableOutboundAdapter to convert between the two +// for use in Hysteria core config. +type PluggableOutbound interface { + TCP(reqAddr *AddrEx) (net.Conn, error) + UDP(reqAddr *AddrEx) (UDPConn, error) +} + +type UDPConn interface { + ReadFrom(b []byte) (int, *AddrEx, error) + WriteTo(b []byte, addr *AddrEx) (int, error) + Close() error +} + +// AddrEx keeps both the original string representation of the address and +// the resolved IP addresses from the resolver, if any. +// The actual outbound implementations can choose to use either the string +// representation or the resolved IP addresses, depending on their capabilities. +// A SOCKS5 outbound, for example, should prefer the string representation +// because SOCKS5 protocol supports sending the hostname to the proxy server +// and let the proxy server do the DNS resolution. +type AddrEx struct { + Host string // String representation of the host, can be an IP or a domain name + Port uint16 + ResolveInfo *ResolveInfo // Only set if there's a resolver in the pipeline +} + +func (a *AddrEx) String() string { + return net.JoinHostPort(a.Host, strconv.Itoa(int(a.Port))) +} + +// ResolveInfo contains the resolved IP addresses from the resolver, and any +// error that occurred during the resolution. +// Note that there could be no error but also no resolved IP addresses, +// or there could be an error but also some resolved IP addresses. +// It's up to the actual outbound implementation to decide how to handle +// these cases. +type ResolveInfo struct { + IPv4 net.IP + IPv6 net.IP + Err error +} + +var _ server.Outbound = (*PluggableOutboundAdapter)(nil) + +type PluggableOutboundAdapter struct { + PluggableOutbound +} + +func (a *PluggableOutboundAdapter) TCP(reqAddr string) (net.Conn, error) { + host, port, err := net.SplitHostPort(reqAddr) + if err != nil { + return nil, err + } + portInt, err := strconv.Atoi(port) + if err != nil { + return nil, err + } + return a.PluggableOutbound.TCP(&AddrEx{ + Host: host, + Port: uint16(portInt), + }) +} + +func (a *PluggableOutboundAdapter) UDP(reqAddr string) (server.UDPConn, error) { + host, port, err := net.SplitHostPort(reqAddr) + if err != nil { + return nil, err + } + portInt, err := strconv.Atoi(port) + if err != nil { + return nil, err + } + conn, err := a.PluggableOutbound.UDP(&AddrEx{ + Host: host, + Port: uint16(portInt), + }) + if err != nil { + return nil, err + } + return &udpConnAdapter{conn}, nil +} + +type udpConnAdapter struct { + UDPConn +} + +func (u *udpConnAdapter) ReadFrom(b []byte) (int, string, error) { + n, addr, err := u.UDPConn.ReadFrom(b) + if addr != nil { + return n, addr.String(), err + } else { + return n, "", err + } +} + +func (u *udpConnAdapter) WriteTo(b []byte, addr string) (int, error) { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return 0, err + } + portInt, err := strconv.Atoi(port) + if err != nil { + return 0, err + } + return u.UDPConn.WriteTo(b, &AddrEx{ + Host: host, + Port: uint16(portInt), + }) +} + +func (u *udpConnAdapter) Close() error { + return u.UDPConn.Close() +} diff --git a/v2rayng/hysteria/extras/outbounds/interface_test.go b/v2rayng/hysteria/extras/outbounds/interface_test.go new file mode 100644 index 0000000000..965b98f4b4 --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/interface_test.go @@ -0,0 +1,50 @@ +package outbounds + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPluggableOutboundAdapter(t *testing.T) { + ob := newMockPluggableOutbound(t) + adapter := &PluggableOutboundAdapter{ob} + + ob.EXPECT().TCP(&AddrEx{ + Host: "only.fans", + Port: 443, + }).Return(nil, nil).Once() + conn, err := adapter.TCP("only.fans:443") + assert.Nil(t, conn) + assert.Nil(t, err) + + mc := newMockUDPConn(t) + mc.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(bs []byte) (int, *AddrEx, error) { + return copy(bs, "gura"), &AddrEx{ + Host: "gura.com", + Port: 2333, + }, nil + }).Once() + mc.EXPECT().WriteTo([]byte("gawr"), &AddrEx{ + Host: "another.hololive.tv", + Port: 1551, + }).Return(4, nil).Once() + ob.EXPECT().UDP(&AddrEx{ + Host: "hololive.tv", + Port: 8999, + }).Return(mc, nil).Once() + + uConn, err := adapter.UDP("hololive.tv:8999") + assert.Nil(t, err) + assert.NotNil(t, uConn) + n, err := uConn.WriteTo([]byte("gawr"), "another.hololive.tv:1551") + assert.Nil(t, err) + assert.Equal(t, 4, n) + bs := make([]byte, 1024) + n, addr, err := uConn.ReadFrom(bs) + assert.Nil(t, err) + assert.Equal(t, 4, n) + assert.Equal(t, "gura", string(bs[:n])) + assert.Equal(t, "gura.com:2333", addr) +} diff --git a/v2rayng/hysteria/extras/outbounds/mock_PluggableOutbound.go b/v2rayng/hysteria/extras/outbounds/mock_PluggableOutbound.go new file mode 100644 index 0000000000..79b38e4295 --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/mock_PluggableOutbound.go @@ -0,0 +1,152 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package outbounds + +import ( + net "net" + + mock "github.com/stretchr/testify/mock" +) + +// mockPluggableOutbound is an autogenerated mock type for the PluggableOutbound type +type mockPluggableOutbound struct { + mock.Mock +} + +type mockPluggableOutbound_Expecter struct { + mock *mock.Mock +} + +func (_m *mockPluggableOutbound) EXPECT() *mockPluggableOutbound_Expecter { + return &mockPluggableOutbound_Expecter{mock: &_m.Mock} +} + +// TCP provides a mock function with given fields: reqAddr +func (_m *mockPluggableOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) { + ret := _m.Called(reqAddr) + + if len(ret) == 0 { + panic("no return value specified for TCP") + } + + var r0 net.Conn + var r1 error + if rf, ok := ret.Get(0).(func(*AddrEx) (net.Conn, error)); ok { + return rf(reqAddr) + } + if rf, ok := ret.Get(0).(func(*AddrEx) net.Conn); ok { + r0 = rf(reqAddr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(net.Conn) + } + } + + if rf, ok := ret.Get(1).(func(*AddrEx) error); ok { + r1 = rf(reqAddr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockPluggableOutbound_TCP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCP' +type mockPluggableOutbound_TCP_Call struct { + *mock.Call +} + +// TCP is a helper method to define mock.On call +// - reqAddr *AddrEx +func (_e *mockPluggableOutbound_Expecter) TCP(reqAddr interface{}) *mockPluggableOutbound_TCP_Call { + return &mockPluggableOutbound_TCP_Call{Call: _e.mock.On("TCP", reqAddr)} +} + +func (_c *mockPluggableOutbound_TCP_Call) Run(run func(reqAddr *AddrEx)) *mockPluggableOutbound_TCP_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*AddrEx)) + }) + return _c +} + +func (_c *mockPluggableOutbound_TCP_Call) Return(_a0 net.Conn, _a1 error) *mockPluggableOutbound_TCP_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockPluggableOutbound_TCP_Call) RunAndReturn(run func(*AddrEx) (net.Conn, error)) *mockPluggableOutbound_TCP_Call { + _c.Call.Return(run) + return _c +} + +// UDP provides a mock function with given fields: reqAddr +func (_m *mockPluggableOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) { + ret := _m.Called(reqAddr) + + if len(ret) == 0 { + panic("no return value specified for UDP") + } + + var r0 UDPConn + var r1 error + if rf, ok := ret.Get(0).(func(*AddrEx) (UDPConn, error)); ok { + return rf(reqAddr) + } + if rf, ok := ret.Get(0).(func(*AddrEx) UDPConn); ok { + r0 = rf(reqAddr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(UDPConn) + } + } + + if rf, ok := ret.Get(1).(func(*AddrEx) error); ok { + r1 = rf(reqAddr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockPluggableOutbound_UDP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDP' +type mockPluggableOutbound_UDP_Call struct { + *mock.Call +} + +// UDP is a helper method to define mock.On call +// - reqAddr *AddrEx +func (_e *mockPluggableOutbound_Expecter) UDP(reqAddr interface{}) *mockPluggableOutbound_UDP_Call { + return &mockPluggableOutbound_UDP_Call{Call: _e.mock.On("UDP", reqAddr)} +} + +func (_c *mockPluggableOutbound_UDP_Call) Run(run func(reqAddr *AddrEx)) *mockPluggableOutbound_UDP_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*AddrEx)) + }) + return _c +} + +func (_c *mockPluggableOutbound_UDP_Call) Return(_a0 UDPConn, _a1 error) *mockPluggableOutbound_UDP_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockPluggableOutbound_UDP_Call) RunAndReturn(run func(*AddrEx) (UDPConn, error)) *mockPluggableOutbound_UDP_Call { + _c.Call.Return(run) + return _c +} + +// newMockPluggableOutbound creates a new instance of mockPluggableOutbound. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockPluggableOutbound(t interface { + mock.TestingT + Cleanup(func()) +}) *mockPluggableOutbound { + mock := &mockPluggableOutbound{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/v2rayng/hysteria/extras/outbounds/mock_UDPConn.go b/v2rayng/hysteria/extras/outbounds/mock_UDPConn.go new file mode 100644 index 0000000000..d450322a4f --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/mock_UDPConn.go @@ -0,0 +1,199 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package outbounds + +import mock "github.com/stretchr/testify/mock" + +// mockUDPConn is an autogenerated mock type for the UDPConn type +type mockUDPConn struct { + mock.Mock +} + +type mockUDPConn_Expecter struct { + mock *mock.Mock +} + +func (_m *mockUDPConn) EXPECT() *mockUDPConn_Expecter { + return &mockUDPConn_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function with given fields: +func (_m *mockUDPConn) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockUDPConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type mockUDPConn_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *mockUDPConn_Expecter) Close() *mockUDPConn_Close_Call { + return &mockUDPConn_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *mockUDPConn_Close_Call) Run(run func()) *mockUDPConn_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *mockUDPConn_Close_Call) Return(_a0 error) *mockUDPConn_Close_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockUDPConn_Close_Call) RunAndReturn(run func() error) *mockUDPConn_Close_Call { + _c.Call.Return(run) + return _c +} + +// ReadFrom provides a mock function with given fields: b +func (_m *mockUDPConn) ReadFrom(b []byte) (int, *AddrEx, error) { + ret := _m.Called(b) + + if len(ret) == 0 { + panic("no return value specified for ReadFrom") + } + + var r0 int + var r1 *AddrEx + var r2 error + if rf, ok := ret.Get(0).(func([]byte) (int, *AddrEx, error)); ok { + return rf(b) + } + if rf, ok := ret.Get(0).(func([]byte) int); ok { + r0 = rf(b) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func([]byte) *AddrEx); ok { + r1 = rf(b) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*AddrEx) + } + } + + if rf, ok := ret.Get(2).(func([]byte) error); ok { + r2 = rf(b) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// mockUDPConn_ReadFrom_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadFrom' +type mockUDPConn_ReadFrom_Call struct { + *mock.Call +} + +// ReadFrom is a helper method to define mock.On call +// - b []byte +func (_e *mockUDPConn_Expecter) ReadFrom(b interface{}) *mockUDPConn_ReadFrom_Call { + return &mockUDPConn_ReadFrom_Call{Call: _e.mock.On("ReadFrom", b)} +} + +func (_c *mockUDPConn_ReadFrom_Call) Run(run func(b []byte)) *mockUDPConn_ReadFrom_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte)) + }) + return _c +} + +func (_c *mockUDPConn_ReadFrom_Call) Return(_a0 int, _a1 *AddrEx, _a2 error) *mockUDPConn_ReadFrom_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *mockUDPConn_ReadFrom_Call) RunAndReturn(run func([]byte) (int, *AddrEx, error)) *mockUDPConn_ReadFrom_Call { + _c.Call.Return(run) + return _c +} + +// WriteTo provides a mock function with given fields: b, addr +func (_m *mockUDPConn) WriteTo(b []byte, addr *AddrEx) (int, error) { + ret := _m.Called(b, addr) + + if len(ret) == 0 { + panic("no return value specified for WriteTo") + } + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func([]byte, *AddrEx) (int, error)); ok { + return rf(b, addr) + } + if rf, ok := ret.Get(0).(func([]byte, *AddrEx) int); ok { + r0 = rf(b, addr) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func([]byte, *AddrEx) error); ok { + r1 = rf(b, addr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockUDPConn_WriteTo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteTo' +type mockUDPConn_WriteTo_Call struct { + *mock.Call +} + +// WriteTo is a helper method to define mock.On call +// - b []byte +// - addr *AddrEx +func (_e *mockUDPConn_Expecter) WriteTo(b interface{}, addr interface{}) *mockUDPConn_WriteTo_Call { + return &mockUDPConn_WriteTo_Call{Call: _e.mock.On("WriteTo", b, addr)} +} + +func (_c *mockUDPConn_WriteTo_Call) Run(run func(b []byte, addr *AddrEx)) *mockUDPConn_WriteTo_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte), args[1].(*AddrEx)) + }) + return _c +} + +func (_c *mockUDPConn_WriteTo_Call) Return(_a0 int, _a1 error) *mockUDPConn_WriteTo_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockUDPConn_WriteTo_Call) RunAndReturn(run func([]byte, *AddrEx) (int, error)) *mockUDPConn_WriteTo_Call { + _c.Call.Return(run) + return _c +} + +// newMockUDPConn creates a new instance of mockUDPConn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockUDPConn(t interface { + mock.TestingT + Cleanup(func()) +}) *mockUDPConn { + mock := &mockUDPConn{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/v2rayng/hysteria/extras/outbounds/ob_direct.go b/v2rayng/hysteria/extras/outbounds/ob_direct.go new file mode 100644 index 0000000000..b80ac00305 --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/ob_direct.go @@ -0,0 +1,460 @@ +package outbounds + +import ( + "errors" + "net" + "strconv" + "time" +) + +type DirectOutboundMode int + +type udpConnState int + +const ( + DirectOutboundModeAuto DirectOutboundMode = iota // Dual-stack "happy eyeballs"-like mode + DirectOutboundMode64 // Use IPv6 address when available, otherwise IPv4 + DirectOutboundMode46 // Use IPv4 address when available, otherwise IPv6 + DirectOutboundMode6 // Use IPv6 only, fail if not available + DirectOutboundMode4 // Use IPv4 only, fail if not available + + defaultDialerTimeout = 10 * time.Second +) + +const ( + udpConnStateDualStack udpConnState = iota + udpConnStateIPv4 + udpConnStateIPv6 +) + +// directOutbound is a PluggableOutbound that connects directly to the target +// using the local network (as opposed to using a proxy, for example). +// It prefers to use ResolveInfo in AddrEx if available. But if it's nil, +// it will fall back to resolving Host using Go's built-in DNS resolver. +type directOutbound struct { + Mode DirectOutboundMode + + // Dialer4 and Dialer6 are used for IPv4 and IPv6 TCP connections respectively. + Dialer4 *net.Dialer + Dialer6 *net.Dialer + + // DeviceName & BindIPs are for UDP connections. They don't use dialers, so we + // need to bind them when creating the connection. + DeviceName string + BindIP4 net.IP + BindIP6 net.IP +} + +type noAddressError struct { + IPv4 bool + IPv6 bool +} + +func (e noAddressError) Error() string { + if e.IPv4 && e.IPv6 { + return "no IPv4 or IPv6 address available" + } else if e.IPv4 { + return "no IPv4 address available" + } else if e.IPv6 { + return "no IPv6 address available" + } else { + return "no address available" + } +} + +type invalidOutboundModeError struct{} + +func (e invalidOutboundModeError) Error() string { + return "invalid outbound mode" +} + +type resolveError struct { + Err error +} + +func (e resolveError) Error() string { + if e.Err == nil { + return "resolve error" + } else { + return "resolve error: " + e.Err.Error() + } +} + +func (e resolveError) Unwrap() error { + return e.Err +} + +// NewDirectOutboundSimple creates a new directOutbound with the given mode, +// without binding to a specific device. Works on all platforms. +func NewDirectOutboundSimple(mode DirectOutboundMode) PluggableOutbound { + d := &net.Dialer{ + Timeout: defaultDialerTimeout, + } + return &directOutbound{ + Mode: mode, + Dialer4: d, + Dialer6: d, + } +} + +// NewDirectOutboundBindToIPs creates a new directOutbound with the given mode, +// and binds to the given IPv4 and IPv6 addresses. Either or both of the addresses +// can be nil, in which case the directOutbound will not bind to a specific address +// for that family. +func NewDirectOutboundBindToIPs(mode DirectOutboundMode, bindIP4, bindIP6 net.IP) (PluggableOutbound, error) { + if bindIP4 != nil && bindIP4.To4() == nil { + return nil, errors.New("bindIP4 must be an IPv4 address") + } + if bindIP6 != nil && bindIP6.To4() != nil { + return nil, errors.New("bindIP6 must be an IPv6 address") + } + ob := &directOutbound{ + Mode: mode, + Dialer4: &net.Dialer{ + Timeout: defaultDialerTimeout, + }, + Dialer6: &net.Dialer{ + Timeout: defaultDialerTimeout, + }, + BindIP4: bindIP4, + BindIP6: bindIP6, + } + if bindIP4 != nil { + ob.Dialer4.LocalAddr = &net.TCPAddr{ + IP: bindIP4, + } + } + if bindIP6 != nil { + ob.Dialer6.LocalAddr = &net.TCPAddr{ + IP: bindIP6, + } + } + return ob, nil +} + +// resolve is our built-in DNS resolver for handling the case when +// AddrEx.ResolveInfo is nil. +func (d *directOutbound) resolve(reqAddr *AddrEx) { + ips, err := net.LookupIP(reqAddr.Host) + if err != nil { + reqAddr.ResolveInfo = &ResolveInfo{Err: err} + return + } + r := &ResolveInfo{} + r.IPv4, r.IPv6 = splitIPv4IPv6(ips) + if r.IPv4 == nil && r.IPv6 == nil { + r.Err = noAddressError{IPv4: true, IPv6: true} + } + reqAddr.ResolveInfo = r +} + +func (d *directOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) { + if reqAddr.ResolveInfo == nil { + // AddrEx.ResolveInfo is nil (no resolver in the pipeline), + // we need to resolve the address ourselves. + d.resolve(reqAddr) + } + r := reqAddr.ResolveInfo + if r.IPv4 == nil && r.IPv6 == nil { + // ResolveInfo not nil but no address available, + // this can only mean that the resolver failed. + // Return the error from the resolver. + return nil, resolveError{Err: r.Err} + } + switch d.Mode { + case DirectOutboundModeAuto: + if r.IPv4 != nil && r.IPv6 != nil { + return d.dualStackDialTCP(r.IPv4, r.IPv6, reqAddr.Port) + } else if r.IPv4 != nil { + return d.dialTCP(r.IPv4, reqAddr.Port) + } else { + return d.dialTCP(r.IPv6, reqAddr.Port) + } + case DirectOutboundMode64: + if r.IPv6 != nil { + return d.dialTCP(r.IPv6, reqAddr.Port) + } else { + return d.dialTCP(r.IPv4, reqAddr.Port) + } + case DirectOutboundMode46: + if r.IPv4 != nil { + return d.dialTCP(r.IPv4, reqAddr.Port) + } else { + return d.dialTCP(r.IPv6, reqAddr.Port) + } + case DirectOutboundMode6: + if r.IPv6 != nil { + return d.dialTCP(r.IPv6, reqAddr.Port) + } else { + return nil, noAddressError{IPv6: true} + } + case DirectOutboundMode4: + if r.IPv4 != nil { + return d.dialTCP(r.IPv4, reqAddr.Port) + } else { + return nil, noAddressError{IPv4: true} + } + default: + return nil, invalidOutboundModeError{} + } +} + +func (d *directOutbound) dialTCP(ip net.IP, port uint16) (net.Conn, error) { + if ip.To4() != nil { + return d.Dialer4.Dial("tcp4", net.JoinHostPort(ip.String(), strconv.Itoa(int(port)))) + } else { + return d.Dialer6.Dial("tcp6", net.JoinHostPort(ip.String(), strconv.Itoa(int(port)))) + } +} + +type dialResult struct { + Conn net.Conn + Err error +} + +// dualStackDialTCP dials the target using both IPv4 and IPv6 addresses simultaneously. +// It returns the first successful connection and drops the other one. +// If both connections fail, it returns the last error. +func (d *directOutbound) dualStackDialTCP(ipv4, ipv6 net.IP, port uint16) (net.Conn, error) { + ch := make(chan dialResult, 2) + go func() { + conn, err := d.dialTCP(ipv4, port) + ch <- dialResult{Conn: conn, Err: err} + }() + go func() { + conn, err := d.dialTCP(ipv6, port) + ch <- dialResult{Conn: conn, Err: err} + }() + // Get the first result, check if it's successful + if r := <-ch; r.Err == nil { + // Yes. Return this and close the other connection when it's done + go func() { + r2 := <-ch + if r2.Conn != nil { + _ = r2.Conn.Close() + } + }() + return r.Conn, nil + } else { + // No. Return the other result, which may or may not be successful + r2 := <-ch + return r2.Conn, r2.Err + } +} + +type directOutboundUDPConn struct { + *directOutbound + *net.UDPConn + State udpConnState +} + +func (u *directOutboundUDPConn) ReadFrom(b []byte) (int, *AddrEx, error) { + n, addr, err := u.UDPConn.ReadFromUDP(b) + if addr != nil { + return n, &AddrEx{ + Host: addr.IP.String(), + Port: uint16(addr.Port), + }, err + } else { + return n, nil, err + } +} + +func (u *directOutboundUDPConn) WriteTo(b []byte, addr *AddrEx) (int, error) { + if addr.ResolveInfo == nil { + u.directOutbound.resolve(addr) + } + r := addr.ResolveInfo + if r.IPv4 == nil && r.IPv6 == nil { + return 0, resolveError{Err: r.Err} + } + if u.State == udpConnStateIPv4 { + if r.IPv4 != nil { + return u.UDPConn.WriteToUDP(b, &net.UDPAddr{ + IP: r.IPv4, + Port: int(addr.Port), + }) + } else { + return 0, noAddressError{IPv4: true} + } + } else if u.State == udpConnStateIPv6 { + if r.IPv6 != nil { + return u.UDPConn.WriteToUDP(b, &net.UDPAddr{ + IP: r.IPv6, + Port: int(addr.Port), + }) + } else { + return 0, noAddressError{IPv6: true} + } + } else { + // Dual stack + switch u.directOutbound.Mode { + case DirectOutboundModeAuto: + // This is a special case. + // We must make a decision here, so we prefer IPv4 for maximum compatibility. + if r.IPv4 != nil { + return u.UDPConn.WriteToUDP(b, &net.UDPAddr{ + IP: r.IPv4, + Port: int(addr.Port), + }) + } else { + return u.UDPConn.WriteToUDP(b, &net.UDPAddr{ + IP: r.IPv6, + Port: int(addr.Port), + }) + } + case DirectOutboundMode64: + if r.IPv6 != nil { + return u.UDPConn.WriteToUDP(b, &net.UDPAddr{ + IP: r.IPv6, + Port: int(addr.Port), + }) + } else { + return u.UDPConn.WriteToUDP(b, &net.UDPAddr{ + IP: r.IPv4, + Port: int(addr.Port), + }) + } + case DirectOutboundMode46: + if r.IPv4 != nil { + return u.UDPConn.WriteToUDP(b, &net.UDPAddr{ + IP: r.IPv4, + Port: int(addr.Port), + }) + } else { + return u.UDPConn.WriteToUDP(b, &net.UDPAddr{ + IP: r.IPv6, + Port: int(addr.Port), + }) + } + case DirectOutboundMode6: + if r.IPv6 != nil { + return u.UDPConn.WriteToUDP(b, &net.UDPAddr{ + IP: r.IPv6, + Port: int(addr.Port), + }) + } else { + return 0, noAddressError{IPv6: true} + } + case DirectOutboundMode4: + if r.IPv4 != nil { + return u.UDPConn.WriteToUDP(b, &net.UDPAddr{ + IP: r.IPv4, + Port: int(addr.Port), + }) + } else { + return 0, noAddressError{IPv4: true} + } + default: + return 0, invalidOutboundModeError{} + } + } +} + +func (u *directOutboundUDPConn) Close() error { + return u.UDPConn.Close() +} + +func (d *directOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) { + if d.BindIP4 == nil && d.BindIP6 == nil { + // No bind address specified, use default dual stack implementation + c, err := net.ListenUDP("udp", nil) + if err != nil { + return nil, err + } + if d.DeviceName != "" { + if err := udpConnBindToDevice(c, d.DeviceName); err != nil { + // Don't forget to close the UDPConn if binding fails + _ = c.Close() + return nil, err + } + } + return &directOutboundUDPConn{ + directOutbound: d, + UDPConn: c, + State: udpConnStateDualStack, + }, nil + } else { + // Bind address specified, + // need to check what kind of address is in reqAddr + // to determine which address family to bind to + if reqAddr.ResolveInfo == nil { + d.resolve(reqAddr) + } + r := reqAddr.ResolveInfo + if r.IPv4 == nil && r.IPv6 == nil { + return nil, resolveError{Err: r.Err} + } + var bindIP net.IP // can be nil, in which case we still lock the address family but don't bind to any address + var state udpConnState // either IPv4 or IPv6 + switch d.Mode { + case DirectOutboundModeAuto: + // This is a special case. + // We must make a decision here, so we prefer IPv4 for maximum compatibility. + if r.IPv4 != nil { + bindIP = d.BindIP4 + state = udpConnStateIPv4 + } else { + bindIP = d.BindIP6 + state = udpConnStateIPv6 + } + case DirectOutboundMode64: + if r.IPv6 != nil { + bindIP = d.BindIP6 + state = udpConnStateIPv6 + } else { + bindIP = d.BindIP4 + state = udpConnStateIPv4 + } + case DirectOutboundMode46: + if r.IPv4 != nil { + bindIP = d.BindIP4 + state = udpConnStateIPv4 + } else { + bindIP = d.BindIP6 + state = udpConnStateIPv6 + } + case DirectOutboundMode6: + if r.IPv6 != nil { + bindIP = d.BindIP6 + state = udpConnStateIPv6 + } else { + return nil, noAddressError{IPv6: true} + } + case DirectOutboundMode4: + if r.IPv4 != nil { + bindIP = d.BindIP4 + state = udpConnStateIPv4 + } else { + return nil, noAddressError{IPv4: true} + } + default: + return nil, invalidOutboundModeError{} + } + var network string + var c *net.UDPConn + var err error + if state == udpConnStateIPv4 { + network = "udp4" + } else { + network = "udp6" + } + if bindIP != nil { + c, err = net.ListenUDP(network, &net.UDPAddr{ + IP: bindIP, + }) + } else { + c, err = net.ListenUDP(network, nil) + } + if err != nil { + return nil, err + } + // We don't support binding to both device & address at the same time, + // so d.DeviceName is ignored in this case. + return &directOutboundUDPConn{ + directOutbound: d, + UDPConn: c, + State: state, + }, nil + } +} diff --git a/v2rayng/hysteria/extras/outbounds/ob_direct_linux.go b/v2rayng/hysteria/extras/outbounds/ob_direct_linux.go new file mode 100644 index 0000000000..33b7d097be --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/ob_direct_linux.go @@ -0,0 +1,57 @@ +package outbounds + +import ( + "errors" + "net" + "syscall" +) + +// NewDirectOutboundBindToDevice creates a new directOutbound with the given mode, +// and binds to the given device. Only works on Linux. +func NewDirectOutboundBindToDevice(mode DirectOutboundMode, deviceName string) (PluggableOutbound, error) { + if err := verifyDeviceName(deviceName); err != nil { + return nil, err + } + d := &net.Dialer{ + Timeout: defaultDialerTimeout, + Control: func(network, address string, c syscall.RawConn) error { + var errBind error + err := c.Control(func(fd uintptr) { + errBind = syscall.BindToDevice(int(fd), deviceName) + }) + if err != nil { + return err + } + return errBind + }, + } + return &directOutbound{ + Mode: mode, + Dialer4: d, + Dialer6: d, + DeviceName: deviceName, + }, nil +} + +func verifyDeviceName(deviceName string) error { + if deviceName == "" { + return errors.New("device name cannot be empty") + } + _, err := net.InterfaceByName(deviceName) + return err +} + +func udpConnBindToDevice(conn *net.UDPConn, deviceName string) error { + sc, err := conn.SyscallConn() + if err != nil { + return err + } + var errBind error + err = sc.Control(func(fd uintptr) { + errBind = syscall.BindToDevice(int(fd), deviceName) + }) + if err != nil { + return err + } + return errBind +} diff --git a/v2rayng/hysteria/extras/outbounds/ob_direct_others.go b/v2rayng/hysteria/extras/outbounds/ob_direct_others.go new file mode 100644 index 0000000000..b416c3023f --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/ob_direct_others.go @@ -0,0 +1,19 @@ +//go:build !linux + +package outbounds + +import ( + "errors" + "net" +) + +// NewDirectOutboundBindToDevice creates a new directOutbound with the given mode, +// and binds to the given device. This doesn't work on non-Linux platforms, so this +// is just a stub function that always returns an error. +func NewDirectOutboundBindToDevice(mode DirectOutboundMode, deviceName string) (PluggableOutbound, error) { + return nil, errors.New("binding to device is not supported on this platform") +} + +func udpConnBindToDevice(conn *net.UDPConn, deviceName string) error { + return errors.New("binding to device is not supported on this platform") +} diff --git a/v2rayng/hysteria/extras/outbounds/ob_http.go b/v2rayng/hysteria/extras/outbounds/ob_http.go new file mode 100644 index 0000000000..48d5aac866 --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/ob_http.go @@ -0,0 +1,190 @@ +package outbounds + +import ( + "bufio" + "bytes" + "crypto/tls" + "encoding/base64" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strconv" + "time" +) + +const ( + httpRequestTimeout = 10 * time.Second +) + +var ( + errHTTPUDPNotSupported = errors.New("UDP not supported by HTTP proxy") + errHTTPUnsupportedScheme = errors.New("unsupported scheme for HTTP proxy (use http:// or https://)") +) + +type errHTTPRequestFailed struct { + Status int +} + +func (e errHTTPRequestFailed) Error() string { + return fmt.Sprintf("HTTP request failed: %d", e.Status) +} + +// httpOutbound is a PluggableOutbound that connects to the target using +// an HTTP/HTTPS proxy server (that supports the CONNECT method). +// HTTP proxies don't support UDP by design, so this outbound will reject +// any UDP request with errHTTPUDPNotSupported. +// Since HTTP proxies support using either IP or domain name as the target +// address, it will ignore ResolveInfo in AddrEx and always only use Host. +type httpOutbound struct { + Dialer *net.Dialer + Addr string + HTTPS bool + Insecure bool + ServerName string + BasicAuth string // This is after Base64 encoding +} + +func NewHTTPOutbound(proxyURL string, insecure bool) (PluggableOutbound, error) { + u, err := url.Parse(proxyURL) + if err != nil { + return nil, err + } + if u.Scheme != "http" && u.Scheme != "https" { + return nil, errHTTPUnsupportedScheme + } + addr := u.Host + if u.Port() == "" { + if u.Scheme == "http" { + addr = net.JoinHostPort(u.Host, "80") + } else { + addr = net.JoinHostPort(u.Host, "443") + } + } + var basicAuth string + if u.User != nil { + username := u.User.Username() + password, _ := u.User.Password() + basicAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password)) + } + return &httpOutbound{ + Dialer: &net.Dialer{Timeout: defaultDialerTimeout}, + Addr: addr, + HTTPS: u.Scheme == "https", + Insecure: insecure, + ServerName: u.Hostname(), + BasicAuth: basicAuth, + }, nil +} + +func (o *httpOutbound) dial() (net.Conn, error) { + conn, err := o.Dialer.Dial("tcp", o.Addr) + if err != nil { + return nil, err + } + if o.HTTPS { + // Wrap the connection with TLS if the proxy is HTTPS. + conn = tls.Client(conn, &tls.Config{ + InsecureSkipVerify: o.Insecure, + ServerName: o.Addr, + }) + } + return conn, nil +} + +func (o *httpOutbound) addrExToRequest(reqAddr *AddrEx) (*http.Request, error) { + req := &http.Request{ + Method: http.MethodConnect, + URL: &url.URL{ + Host: net.JoinHostPort(reqAddr.Host, strconv.Itoa(int(reqAddr.Port))), + }, + Header: http.Header{ + "Proxy-Connection": []string{"Keep-Alive"}, + }, + } + if o.BasicAuth != "" { + req.Header.Add("Proxy-Authorization", o.BasicAuth) + } + return req, nil +} + +func (o *httpOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) { + req, err := o.addrExToRequest(reqAddr) + if err != nil { + return nil, err + } + conn, err := o.dial() + if err != nil { + return nil, err + } + if err := req.Write(conn); err != nil { + _ = conn.Close() + return nil, err + } + if err := conn.SetDeadline(time.Now().Add(httpRequestTimeout)); err != nil { + _ = conn.Close() + return nil, err + } + bufReader := bufio.NewReader(conn) + resp, err := http.ReadResponse(bufReader, req) + if resp != nil { + // Don't need response body here. + _ = resp.Body.Close() + } + if err != nil { + _ = conn.Close() + return nil, err + } + if resp.StatusCode != http.StatusOK { + _ = conn.Close() + return nil, errHTTPRequestFailed{resp.StatusCode} + } + if err := conn.SetDeadline(time.Time{}); err != nil { + _ = conn.Close() + return nil, err + } + if bufReader.Buffered() > 0 { + // There is still data in the buffered reader. + // We need to get it out and put it into a cachedConn, + // so that handleConnect can read it. + data := make([]byte, bufReader.Buffered()) + _, err := io.ReadFull(bufReader, data) + if err != nil { + // Read from buffer failed, is this possible? + _ = conn.Close() + return nil, err + } + cachedConn := &cachedConn{ + Conn: conn, + Buffer: *bytes.NewBuffer(data), + } + return cachedConn, nil + } else { + return conn, nil + } +} + +func (o *httpOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) { + return nil, errHTTPUDPNotSupported +} + +// cachedConn is a net.Conn wrapper that first Read()s from a buffer, +// and then from the underlying net.Conn when the buffer is drained. +type cachedConn struct { + net.Conn + Buffer bytes.Buffer +} + +func (c *cachedConn) Read(b []byte) (int, error) { + if c.Buffer.Len() > 0 { + n, err := c.Buffer.Read(b) + if err == io.EOF { + // Buffer is drained, hide it from the caller + err = nil + } + return n, err + } + return c.Conn.Read(b) +} diff --git a/v2rayng/hysteria/extras/outbounds/ob_socks5.go b/v2rayng/hysteria/extras/outbounds/ob_socks5.go new file mode 100644 index 0000000000..d6d7add891 --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/ob_socks5.go @@ -0,0 +1,281 @@ +package outbounds + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + "net" + "time" + + "github.com/txthinking/socks5" +) + +const ( + socks5NegotiationTimeout = 10 * time.Second + socks5RequestTimeout = 10 * time.Second +) + +var errSOCKS5AuthFailed = errors.New("SOCKS5 authentication failed") + +type errSOCKS5UnsupportedAuthMethod struct { + Method byte +} + +func (e errSOCKS5UnsupportedAuthMethod) Error() string { + return fmt.Sprintf("unsupported SOCKS5 authentication method: %d", e.Method) +} + +type errSOCKS5RequestFailed struct { + Rep byte +} + +func (e errSOCKS5RequestFailed) Error() string { + var msg string + // RFC 1928 + switch e.Rep { + case 0x00: + msg = "succeeded" + case 0x01: + msg = "general SOCKS server failure" + case 0x02: + msg = "connection not allowed by ruleset" + case 0x03: + msg = "Network unreachable" + case 0x04: + msg = "Host unreachable" + case 0x05: + msg = "Connection refused" + case 0x06: + msg = "TTL expired" + case 0x07: + msg = "Command not supported" + case 0x08: + msg = "Address type not supported" + default: + msg = "undefined" + } + return fmt.Sprintf("SOCKS5 request failed: %s (%d)", msg, e.Rep) +} + +// socks5Outbound is a PluggableOutbound that connects to the target using +// a SOCKS5 proxy server. +// Since SOCKS5 supports using either IP or domain name as the target address, +// it will ignore ResolveInfo in AddrEx and always only use Host. +type socks5Outbound struct { + Dialer *net.Dialer + Addr string + Username string + Password string +} + +func NewSOCKS5Outbound(addr, username, password string) PluggableOutbound { + return &socks5Outbound{ + Dialer: &net.Dialer{ + Timeout: defaultDialerTimeout, + }, + Addr: addr, + Username: username, + Password: password, + } +} + +// dialAndNegotiate creates a new TCP connection to the SOCKS5 proxy server +// and performs the negotiation. Returns an established connection ready to +// handle requests, or an error if the process fails. +func (o *socks5Outbound) dialAndNegotiate() (net.Conn, error) { + conn, err := o.Dialer.Dial("tcp", o.Addr) + if err != nil { + return nil, err + } + if err := conn.SetDeadline(time.Now().Add(socks5NegotiationTimeout)); err != nil { + _ = conn.Close() + return nil, err + } + authMethods := []byte{socks5.MethodNone} + if o.Username != "" && o.Password != "" { + authMethods = append(authMethods, socks5.MethodUsernamePassword) + } + req := socks5.NewNegotiationRequest(authMethods) + if _, err := req.WriteTo(conn); err != nil { + _ = conn.Close() + return nil, err + } + resp, err := socks5.NewNegotiationReplyFrom(conn) + if err != nil { + _ = conn.Close() + return nil, err + } + if resp.Method == socks5.MethodUsernamePassword { + upReq := socks5.NewUserPassNegotiationRequest([]byte(o.Username), []byte(o.Password)) + if _, err := upReq.WriteTo(conn); err != nil { + _ = conn.Close() + return nil, err + } + upResp, err := socks5.NewUserPassNegotiationReplyFrom(conn) + if err != nil { + _ = conn.Close() + return nil, err + } + if upResp.Status != socks5.UserPassStatusSuccess { + _ = conn.Close() + return nil, errSOCKS5AuthFailed + } + } else if resp.Method != socks5.MethodNone { + // We only support none & username/password authentication methods. + _ = conn.Close() + return nil, errSOCKS5UnsupportedAuthMethod{resp.Method} + } + // Negotiation succeeded, reset the deadline. + if err := conn.SetDeadline(time.Time{}); err != nil { + _ = conn.Close() + return nil, err + } + return conn, nil +} + +// request sends a SOCKS5 request to the proxy server and returns the reply. +// Note that it will return an error if the reply from the server indicates +// a failure. +func (o *socks5Outbound) request(conn net.Conn, req *socks5.Request) (*socks5.Reply, error) { + if err := conn.SetDeadline(time.Now().Add(socks5RequestTimeout)); err != nil { + return nil, err + } + if _, err := req.WriteTo(conn); err != nil { + return nil, err + } + resp, err := socks5.NewReplyFrom(conn) + if err != nil { + return nil, err + } + if resp.Rep != socks5.RepSuccess { + return nil, errSOCKS5RequestFailed{resp.Rep} + } + if err := conn.SetDeadline(time.Time{}); err != nil { + return nil, err + } + return resp, nil +} + +func (s *socks5Outbound) TCP(reqAddr *AddrEx) (net.Conn, error) { + conn, err := s.dialAndNegotiate() + if err != nil { + return nil, err + } + atyp, dstAddr, dstPort := addrExToSOCKS5Addr(reqAddr) + req := socks5.NewRequest(socks5.CmdConnect, atyp, dstAddr, dstPort) + if _, err := s.request(conn, req); err != nil { + _ = conn.Close() + return nil, err + } + return conn, nil +} + +func (s *socks5Outbound) UDP(reqAddr *AddrEx) (UDPConn, error) { + conn, err := s.dialAndNegotiate() + if err != nil { + return nil, err + } + atyp, dstAddr, dstPort := addrExToSOCKS5Addr(reqAddr) + req := socks5.NewRequest(socks5.CmdUDP, atyp, dstAddr, dstPort) + resp, err := s.request(conn, req) + if err != nil { + _ = conn.Close() + return nil, err + } + return newSOCKS5UDPConn(conn, resp.Address()) +} + +type socks5UDPConn struct { + tcpConn net.Conn + udpConn net.Conn +} + +func newSOCKS5UDPConn(tcpConn net.Conn, udpAddr string) (*socks5UDPConn, error) { + udpConn, err := net.Dial("udp", udpAddr) + if err != nil { + return nil, err + } + sc := &socks5UDPConn{ + tcpConn: tcpConn, + udpConn: udpConn, + } + go sc.hold() + return sc, nil +} + +func (c *socks5UDPConn) hold() { + _, _ = io.Copy(io.Discard, c.tcpConn) + _ = c.tcpConn.Close() + _ = c.udpConn.Close() +} + +func (c *socks5UDPConn) ReadFrom(b []byte) (int, *AddrEx, error) { + n, err := c.udpConn.Read(b) + if err != nil { + return 0, nil, err + } + d, err := socks5.NewDatagramFromBytes(b[:n]) + if err != nil { + return 0, nil, err + } + addr := socks5AddrToAddrEx(d.Atyp, d.DstAddr, d.DstPort) + n = copy(b, d.Data) + return n, addr, nil +} + +func (c *socks5UDPConn) WriteTo(b []byte, addr *AddrEx) (int, error) { + atyp, dstAddr, dstPort := addrExToSOCKS5Addr(addr) + d := socks5.NewDatagram(atyp, dstAddr, dstPort, b) + _, err := c.udpConn.Write(d.Bytes()) + if err != nil { + return 0, err + } + return len(b), nil +} + +func (c *socks5UDPConn) Close() error { + _ = c.tcpConn.Close() + _ = c.udpConn.Close() + return nil +} + +func addrExToSOCKS5Addr(addr *AddrEx) (atyp byte, dstAddr, dstPort []byte) { + // Host + ip := net.ParseIP(addr.Host) + if ip != nil { + if ip.To4() != nil { + atyp = socks5.ATYPIPv4 + dstAddr = ip.To4() + } else { + atyp = socks5.ATYPIPv6 + dstAddr = ip.To16() + } + } else { + atyp = socks5.ATYPDomain + dstAddr = []byte(addr.Host) + } + // Port + dstPort = make([]byte, 2) + binary.BigEndian.PutUint16(dstPort, addr.Port) + return +} + +func socks5AddrToAddrEx(atyp byte, dstAddr, dstPort []byte) *AddrEx { + // Host + var host string + if atyp == socks5.ATYPIPv4 { + host = net.IP(dstAddr).To4().String() + } else if atyp == socks5.ATYPIPv6 { + host = net.IP(dstAddr).To16().String() + } else if atyp == socks5.ATYPDomain { + // Need to strip the first byte which is the domain length. + host = string(dstAddr[1:]) + } + // Port + port := binary.BigEndian.Uint16(dstPort) + return &AddrEx{ + Host: host, + Port: port, + } +} diff --git a/v2rayng/hysteria/extras/outbounds/speedtest.go b/v2rayng/hysteria/extras/outbounds/speedtest.go new file mode 100644 index 0000000000..162f4dcdf4 --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/speedtest.go @@ -0,0 +1,36 @@ +package outbounds + +import ( + "net" + + "github.com/apernet/hysteria/extras/v2/outbounds/speedtest" +) + +const ( + SpeedtestDest = "@SpeedTest" +) + +// speedtestHandler is a PluggableOutbound that handles speed test requests. +// It's used to intercept speed test requests and return a pseudo connection that +// implements the speed test protocol. +type speedtestHandler struct { + Next PluggableOutbound +} + +func NewSpeedtestHandler(next PluggableOutbound) PluggableOutbound { + return &speedtestHandler{ + Next: next, + } +} + +func (s *speedtestHandler) TCP(reqAddr *AddrEx) (net.Conn, error) { + if reqAddr.Host == SpeedtestDest { + return speedtest.NewServerConn(), nil + } else { + return s.Next.TCP(reqAddr) + } +} + +func (s *speedtestHandler) UDP(reqAddr *AddrEx) (UDPConn, error) { + return s.Next.UDP(reqAddr) +} diff --git a/v2rayng/hysteria/extras/outbounds/speedtest/client.go b/v2rayng/hysteria/extras/outbounds/speedtest/client.go new file mode 100644 index 0000000000..ea4c5a603d --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/speedtest/client.go @@ -0,0 +1,125 @@ +package speedtest + +import ( + "fmt" + "io" + "net" + "sync/atomic" + "time" +) + +type Client struct { + Conn net.Conn +} + +// Download requests the server to send l bytes of data. +// The callback function cb is called every second with the time since the last call, +// and the number of bytes received in that time. +func (c *Client) Download(l uint32, cb func(time.Duration, uint32, bool)) error { + err := writeDownloadRequest(c.Conn, l) + if err != nil { + return err + } + ok, msg, err := readDownloadResponse(c.Conn) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("server rejected download request: %s", msg) + } + var counter uint32 + stopChan := make(chan struct{}) + defer close(stopChan) + // Call the callback function every second, + // with the time since the last call and the number of bytes received in that time. + go func() { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + t := time.Now() + for { + select { + case <-stopChan: + return + case <-ticker.C: + cb(time.Since(t), atomic.SwapUint32(&counter, 0), false) + t = time.Now() + } + } + }() + buf := make([]byte, chunkSize) + startTime := time.Now() + remaining := l + for remaining > 0 { + n := remaining + if n > chunkSize { + n = chunkSize + } + rn, err := c.Conn.Read(buf[:n]) + remaining -= uint32(rn) + atomic.AddUint32(&counter, uint32(rn)) + if err != nil && !(remaining == 0 && err == io.EOF) { + return err + } + } + // One last call to the callback function to report the total time and bytes received. + cb(time.Since(startTime), l, true) + return nil +} + +// Upload requests the server to receive l bytes of data. +// The callback function cb is called every second with the time since the last call, +// and the number of bytes sent in that time. +func (c *Client) Upload(l uint32, cb func(time.Duration, uint32, bool)) error { + err := writeUploadRequest(c.Conn, l) + if err != nil { + return err + } + ok, msg, err := readUploadResponse(c.Conn) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("server rejected upload request: %s", msg) + } + var counter uint32 + stopChan := make(chan struct{}) + defer close(stopChan) + // Call the callback function every second, + // with the time since the last call and the number of bytes sent in that time. + go func() { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + t := time.Now() + for { + select { + case <-stopChan: + return + case <-ticker.C: + cb(time.Since(t), atomic.SwapUint32(&counter, 0), false) + t = time.Now() + } + } + }() + buf := make([]byte, chunkSize) + remaining := l + for remaining > 0 { + n := remaining + if n > chunkSize { + n = chunkSize + } + _, err := c.Conn.Write(buf[:n]) + if err != nil { + return err + } + remaining -= n + atomic.AddUint32(&counter, n) + } + // Now we should receive the upload summary from the server. + elapsed, received, err := readUploadSummary(c.Conn) + if err != nil { + return err + } + // One last call to the callback function to report the total time and bytes sent. + cb(elapsed, received, true) + return nil +} diff --git a/v2rayng/hysteria/extras/outbounds/speedtest/protocol.go b/v2rayng/hysteria/extras/outbounds/speedtest/protocol.go new file mode 100644 index 0000000000..8d1adb8e33 --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/speedtest/protocol.go @@ -0,0 +1,152 @@ +package speedtest + +import ( + "encoding/binary" + "io" + "time" +) + +const ( + typeDownload = 0x1 + typeUpload = 0x2 +) + +// DownloadRequest format: +// 0x1 (byte) +// Request data length (uint32 BE) + +func readDownloadRequest(r io.Reader) (uint32, error) { + var l uint32 + err := binary.Read(r, binary.BigEndian, &l) + return l, err +} + +func writeDownloadRequest(w io.Writer, l uint32) error { + buf := make([]byte, 5) + buf[0] = typeDownload + binary.BigEndian.PutUint32(buf[1:], l) + _, err := w.Write(buf) + return err +} + +// DownloadResponse format: +// Status (byte, 0=ok, 1=error) +// Message length (uint16 BE) +// Message (bytes) + +func readDownloadResponse(r io.Reader) (bool, string, error) { + var status [1]byte + if _, err := io.ReadFull(r, status[:]); err != nil { + return false, "", err + } + var msgLen uint16 + if err := binary.Read(r, binary.BigEndian, &msgLen); err != nil { + return false, "", err + } + // No message is fine + if msgLen == 0 { + return status[0] == 0, "", nil + } + msgBuf := make([]byte, msgLen) + _, err := io.ReadFull(r, msgBuf) + if err != nil { + return false, "", err + } + return status[0] == 0, string(msgBuf), nil +} + +func writeDownloadResponse(w io.Writer, ok bool, msg string) error { + sz := 1 + 2 + len(msg) + buf := make([]byte, sz) + if ok { + buf[0] = 0 + } else { + buf[0] = 1 + } + binary.BigEndian.PutUint16(buf[1:], uint16(len(msg))) + copy(buf[3:], msg) + _, err := w.Write(buf) + return err +} + +// UploadRequest format: +// 0x2 (byte) +// Upload data length (uint32 BE) + +func readUploadRequest(r io.Reader) (uint32, error) { + var l uint32 + err := binary.Read(r, binary.BigEndian, &l) + return l, err +} + +func writeUploadRequest(w io.Writer, l uint32) error { + buf := make([]byte, 5) + buf[0] = typeUpload + binary.BigEndian.PutUint32(buf[1:], l) + _, err := w.Write(buf) + return err +} + +// UploadResponse format: +// Status (byte, 0=ok, 1=error) +// Message length (uint16 BE) +// Message (bytes) + +func readUploadResponse(r io.Reader) (bool, string, error) { + var status [1]byte + if _, err := io.ReadFull(r, status[:]); err != nil { + return false, "", err + } + var msgLen uint16 + if err := binary.Read(r, binary.BigEndian, &msgLen); err != nil { + return false, "", err + } + // No message is fine + if msgLen == 0 { + return status[0] == 0, "", nil + } + msgBuf := make([]byte, msgLen) + _, err := io.ReadFull(r, msgBuf) + if err != nil { + return false, "", err + } + return status[0] == 0, string(msgBuf), nil +} + +func writeUploadResponse(w io.Writer, ok bool, msg string) error { + sz := 1 + 2 + len(msg) + buf := make([]byte, sz) + if ok { + buf[0] = 0 + } else { + buf[0] = 1 + } + binary.BigEndian.PutUint16(buf[1:], uint16(len(msg))) + copy(buf[3:], msg) + _, err := w.Write(buf) + return err +} + +// UploadSummary format: +// Duration (in milliseconds, uint32 BE) +// Received data length (uint32 BE) + +func readUploadSummary(r io.Reader) (time.Duration, uint32, error) { + var duration uint32 + if err := binary.Read(r, binary.BigEndian, &duration); err != nil { + return 0, 0, err + } + var l uint32 + if err := binary.Read(r, binary.BigEndian, &l); err != nil { + return 0, 0, err + } + return time.Duration(duration) * time.Millisecond, l, nil +} + +func writeUploadSummary(w io.Writer, duration time.Duration, l uint32) error { + buf := make([]byte, 8) + binary.BigEndian.PutUint32(buf, uint32(duration/time.Millisecond)) + binary.BigEndian.PutUint32(buf[4:], l) + _, err := w.Write(buf) + return err +} diff --git a/v2rayng/hysteria/extras/outbounds/speedtest/protocol_test.go b/v2rayng/hysteria/extras/outbounds/speedtest/protocol_test.go new file mode 100644 index 0000000000..1ad23a420b --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/speedtest/protocol_test.go @@ -0,0 +1,446 @@ +package speedtest + +import ( + "bytes" + "testing" + "time" +) + +func TestReadDownloadRequest(t *testing.T) { + tests := []struct { + name string + data []byte + want uint32 + wantErr bool + }{ + { + name: "normal", + data: []byte{0x0, 0x1, 0xBD, 0xC2}, + want: 114114, + wantErr: false, + }, + { + name: "normal zero", + data: []byte{0x0, 0x0, 0x0, 0x0}, + want: 0, + wantErr: false, + }, + { + name: "incomplete", + data: []byte{0x0, 0x1, 0x2}, + want: 0, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := bytes.NewReader(tt.data) + got, err := readDownloadRequest(r) + if (err != nil) != tt.wantErr { + t.Errorf("readDownloadRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("readDownloadRequest() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestWriteDownloadRequest(t *testing.T) { + tests := []struct { + name string + l uint32 + wantW string + wantErr bool + }{ + { + name: "normal", + l: 78909912, + wantW: "\x01\x04\xB4\x11\xD8", + wantErr: false, + }, + { + name: "normal zero", + l: 0, + wantW: "\x01\x00\x00\x00\x00", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &bytes.Buffer{} + err := writeDownloadRequest(w, tt.l) + if (err != nil) != tt.wantErr { + t.Errorf("writeDownloadRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotW := w.String(); gotW != tt.wantW { + t.Errorf("writeDownloadRequest() gotW = %v, want %v", gotW, tt.wantW) + } + }) + } +} + +func TestReadDownloadResponse(t *testing.T) { + tests := []struct { + name string + data []byte + want bool + want1 string + wantErr bool + }{ + { + name: "normal ok", + data: []byte{0x0, 0x0, 0x2, 0x41, 0x42}, + want: true, + want1: "AB", + wantErr: false, + }, + { + name: "normal ok no message", + data: []byte{0x0, 0x0, 0x0, 0x0}, + want: true, + want1: "", + wantErr: false, + }, + { + name: "normal error", + data: []byte{0x1, 0x0, 0x2, 0x43, 0x44}, + want: false, + want1: "CD", + wantErr: false, + }, + { + name: "incomplete", + data: []byte{0x0, 0x99, 0x99, 0x45, 0x46, 0x47}, + want: false, + want1: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := bytes.NewReader(tt.data) + got, got1, err := readDownloadResponse(r) + if (err != nil) != tt.wantErr { + t.Errorf("readDownloadResponse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("readDownloadResponse() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("readDownloadResponse() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestWriteDownloadResponse(t *testing.T) { + type args struct { + ok bool + msg string + } + tests := []struct { + name string + args args + wantW string + wantErr bool + }{ + { + name: "normal ok", + args: args{ok: true, msg: "wahaha"}, + wantW: "\x00\x00\x06wahaha", + wantErr: false, + }, + { + name: "normal error", + args: args{ok: false, msg: "bullbull"}, + wantW: "\x01\x00\x08bullbull", + wantErr: false, + }, + { + name: "empty ok", + args: args{ok: true, msg: ""}, + wantW: "\x00\x00\x00", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &bytes.Buffer{} + err := writeDownloadResponse(w, tt.args.ok, tt.args.msg) + if (err != nil) != tt.wantErr { + t.Errorf("writeDownloadResponse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotW := w.String(); gotW != tt.wantW { + t.Errorf("writeDownloadResponse() gotW = %v, want %v", gotW, tt.wantW) + } + }) + } +} + +func TestReadUploadRequest(t *testing.T) { + tests := []struct { + name string + data []byte + want uint32 + wantErr bool + }{ + { + name: "normal", + data: []byte{0x0, 0x0, 0x26, 0xEE}, + want: 9966, + wantErr: false, + }, + { + name: "normal zero", + data: []byte{0x0, 0x0, 0x0, 0x0}, + want: 0, + wantErr: false, + }, + { + name: "incomplete", + data: []byte{0x1}, + want: 0, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := bytes.NewReader(tt.data) + got, err := readUploadRequest(r) + if (err != nil) != tt.wantErr { + t.Errorf("readUploadRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("readUploadRequest() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestWriteUploadRequest(t *testing.T) { + tests := []struct { + name string + l uint32 + wantW string + wantErr bool + }{ + { + name: "normal", + l: 2291758882, + wantW: "\x02\x88\x99\x77\x22", + wantErr: false, + }, + { + name: "normal zero", + l: 0, + wantW: "\x02\x00\x00\x00\x00", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &bytes.Buffer{} + err := writeUploadRequest(w, tt.l) + if (err != nil) != tt.wantErr { + t.Errorf("writeUploadRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotW := w.String(); gotW != tt.wantW { + t.Errorf("writeUploadRequest() gotW = %v, want %v", gotW, tt.wantW) + } + }) + } +} + +func TestReadUploadResponse(t *testing.T) { + tests := []struct { + name string + data []byte + want bool + want1 string + wantErr bool + }{ + { + name: "normal ok", + data: []byte{0x0, 0x0, 0x2, 0x41, 0x42}, + want: true, + want1: "AB", + wantErr: false, + }, + { + name: "normal ok no message", + data: []byte{0x0, 0x0, 0x0}, + want: true, + want1: "", + wantErr: false, + }, + { + name: "normal error", + data: []byte{0x1, 0x0, 0x2, 0x43, 0x44}, + want: false, + want1: "CD", + wantErr: false, + }, + { + name: "incomplete", + data: []byte{0x0, 0x99, 0x99, 0x45, 0x46, 0x47}, + want: false, + want1: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := bytes.NewReader(tt.data) + got, got1, err := readUploadResponse(r) + if (err != nil) != tt.wantErr { + t.Errorf("readUploadResponse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("readUploadResponse() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("readUploadResponse() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestWriteUploadResponse(t *testing.T) { + type args struct { + ok bool + msg string + } + tests := []struct { + name string + args args + wantW string + wantErr bool + }{ + { + name: "normal ok", + args: args{ok: true, msg: "lul"}, + wantW: "\x00\x00\x03lul", + wantErr: false, + }, + { + name: "normal error", + args: args{ok: false, msg: "notforu"}, + wantW: "\x01\x00\x07notforu", + wantErr: false, + }, + { + name: "empty ok", + args: args{ok: true, msg: ""}, + wantW: "\x00\x00\x00", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &bytes.Buffer{} + err := writeUploadResponse(w, tt.args.ok, tt.args.msg) + if (err != nil) != tt.wantErr { + t.Errorf("writeUploadResponse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotW := w.String(); gotW != tt.wantW { + t.Errorf("writeUploadResponse() gotW = %v, want %v", gotW, tt.wantW) + } + }) + } +} + +func TestReadUploadSummary(t *testing.T) { + tests := []struct { + name string + data []byte + want time.Duration + want1 uint32 + wantErr bool + }{ + { + name: "normal", + data: []byte{0x0, 0x0, 0x14, 0x6E, 0x0, 0x26, 0x25, 0xA0}, + want: 5230 * time.Millisecond, + want1: 2500000, + wantErr: false, + }, + { + name: "zero", + data: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, + want: 0, + want1: 0, + wantErr: false, + }, + { + name: "incomplete", + data: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, + want: 0, + want1: 0, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := bytes.NewReader(tt.data) + got, got1, err := readUploadSummary(r) + if (err != nil) != tt.wantErr { + t.Errorf("readUploadSummary() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("readUploadSummary() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("readUploadSummary() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestWriteUploadSummary(t *testing.T) { + type args struct { + duration time.Duration + l uint32 + } + tests := []struct { + name string + args args + wantW string + wantErr bool + }{ + { + name: "normal", + args: args{duration: 5230 * time.Millisecond, l: 2500000}, + wantW: "\x00\x00\x14\x6E\x00\x26\x25\xA0", + wantErr: false, + }, + { + name: "zero", + args: args{duration: 0, l: 0}, + wantW: "\x00\x00\x00\x00\x00\x00\x00\x00", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &bytes.Buffer{} + err := writeUploadSummary(w, tt.args.duration, tt.args.l) + if (err != nil) != tt.wantErr { + t.Errorf("writeUploadSummary() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotW := w.String(); gotW != tt.wantW { + t.Errorf("writeUploadSummary() gotW = %v, want %v", gotW, tt.wantW) + } + }) + } +} diff --git a/v2rayng/hysteria/extras/outbounds/speedtest/server.go b/v2rayng/hysteria/extras/outbounds/speedtest/server.go new file mode 100644 index 0000000000..d280b57d3f --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/speedtest/server.go @@ -0,0 +1,99 @@ +package speedtest + +import ( + "crypto/rand" + "fmt" + "io" + "net" + "time" +) + +const ( + chunkSize = 64 * 1024 +) + +// NewServerConn creates a new "pseudo" connection that implements the speed test protocol. +// It's called "pseudo" because it's not a real TCP connection - everything is done in memory. +func NewServerConn() net.Conn { + rConn, iConn := net.Pipe() // return conn & internal conn + // Start the server logic + go server(iConn) + return rConn +} + +func server(conn net.Conn) error { + defer conn.Close() + // First byte determines the request type + var typ [1]byte + if _, err := io.ReadFull(conn, typ[:]); err != nil { + return err + } + switch typ[0] { + case typeDownload: + return handleDownload(conn) + case typeUpload: + return handleUpload(conn) + default: + return fmt.Errorf("unknown request type: %d", typ[0]) + } +} + +// handleDownload reads the download request and sends the requested amount of data. +func handleDownload(conn net.Conn) error { + l, err := readDownloadRequest(conn) + if err != nil { + return err + } + err = writeDownloadResponse(conn, true, "OK") + if err != nil { + return err + } + buf := make([]byte, chunkSize) + // Fill the buffer with random data. + // For now, we only do it once and repeat the same data for performance reasons. + _, err = rand.Read(buf) + if err != nil { + return err + } + remaining := l + for remaining > 0 { + n := remaining + if n > chunkSize { + n = chunkSize + } + _, err := conn.Write(buf[:n]) + if err != nil { + return err + } + remaining -= n + } + return nil +} + +// handleUpload reads the upload request, reads & discards the requested amount of data, +// and sends the upload summary. +func handleUpload(conn net.Conn) error { + l, err := readUploadRequest(conn) + if err != nil { + return err + } + err = writeUploadResponse(conn, true, "OK") + if err != nil { + return err + } + buf := make([]byte, chunkSize) + startTime := time.Now() + remaining := l + for remaining > 0 { + n := remaining + if n > chunkSize { + n = chunkSize + } + rn, err := conn.Read(buf[:n]) + remaining -= uint32(rn) + if err != nil && !(remaining == 0 && err == io.EOF) { + return err + } + } + return writeUploadSummary(conn, time.Since(startTime), l) +} diff --git a/v2rayng/hysteria/extras/outbounds/utils.go b/v2rayng/hysteria/extras/outbounds/utils.go new file mode 100644 index 0000000000..7a693cf196 --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/utils.go @@ -0,0 +1,40 @@ +package outbounds + +import "net" + +// splitIPv4IPv6 gets the first IPv4 and IPv6 address from a list of IP addresses. +// Both of the return values can be nil when no IPv4 or IPv6 address is found. +func splitIPv4IPv6(ips []net.IP) (ipv4, ipv6 net.IP) { + for _, ip := range ips { + if ip.To4() != nil { + if ipv4 == nil { + ipv4 = ip + } + } else { + if ipv6 == nil { + ipv6 = ip + } + } + if ipv4 != nil && ipv6 != nil { + // We have everything we need. + break + } + } + return +} + +// tryParseIP tries to parse the host string in the AddrEx as an IP address. +// If the host is indeed an IP address, it will fill the ResolveInfo with the +// parsed IP address and return true. Otherwise, it will return false. +func tryParseIP(addr *AddrEx) bool { + if ip := net.ParseIP(addr.Host); ip != nil { + addr.ResolveInfo = &ResolveInfo{} + if ip.To4() != nil { + addr.ResolveInfo.IPv4 = ip + } else { + addr.ResolveInfo.IPv6 = ip + } + return true + } + return false +} diff --git a/v2rayng/hysteria/extras/outbounds/utils_test.go b/v2rayng/hysteria/extras/outbounds/utils_test.go new file mode 100644 index 0000000000..280a600136 --- /dev/null +++ b/v2rayng/hysteria/extras/outbounds/utils_test.go @@ -0,0 +1,84 @@ +package outbounds + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSplitIPv4IPv6(t *testing.T) { + type args struct { + ips []net.IP + } + tests := []struct { + name string + args args + wantIpv4 net.IP + wantIpv6 net.IP + }{ + { + name: "IPv4 only", + args: args{ + ips: []net.IP{ + net.ParseIP("4.5.6.7"), + net.ParseIP("9.9.9.9"), + }, + }, + wantIpv4: net.ParseIP("4.5.6.7"), + wantIpv6: nil, + }, + { + name: "IPv6 only", + args: args{ + ips: []net.IP{ + net.ParseIP("2001:db8::68"), + net.ParseIP("2001:db8::69"), + }, + }, + wantIpv4: nil, + wantIpv6: net.ParseIP("2001:db8::68"), + }, + { + name: "Both 1", + args: args{ + ips: []net.IP{ + net.ParseIP("2001:db8::68"), + net.ParseIP("2001:db8::69"), + net.ParseIP("4.5.6.7"), + net.ParseIP("9.9.9.9"), + }, + }, + wantIpv4: net.ParseIP("4.5.6.7"), + wantIpv6: net.ParseIP("2001:db8::68"), + }, + { + name: "Both 2", + args: args{ + ips: []net.IP{ + net.ParseIP("2001:db8::69"), + net.ParseIP("9.9.9.9"), + net.ParseIP("2001:db8::68"), + net.ParseIP("4.5.6.7"), + }, + }, + wantIpv4: net.ParseIP("9.9.9.9"), + wantIpv6: net.ParseIP("2001:db8::69"), + }, + { + name: "Empty", + args: args{ + ips: []net.IP{}, + }, + wantIpv4: nil, + wantIpv6: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotIpv4, gotIpv6 := splitIPv4IPv6(tt.args.ips) + assert.Equalf(t, tt.wantIpv4, gotIpv4, "splitIPv4IPv6(%v)", tt.args.ips) + assert.Equalf(t, tt.wantIpv6, gotIpv6, "splitIPv4IPv6(%v)", tt.args.ips) + }) + } +} diff --git a/v2rayng/hysteria/extras/sniff/.mockery.yaml b/v2rayng/hysteria/extras/sniff/.mockery.yaml new file mode 100644 index 0000000000..c866d1da1c --- /dev/null +++ b/v2rayng/hysteria/extras/sniff/.mockery.yaml @@ -0,0 +1,12 @@ +with-expecter: true +dir: . +outpkg: sniff +packages: + github.com/apernet/quic-go: + interfaces: + Stream: + config: + mockname: mockStream + replace-type: # internal package alias dirty fix + - github.com/apernet/quic-go/internal/protocol=github.com/apernet/quic-go + - github.com/apernet/quic-go/internal/qerr=github.com/apernet/quic-go diff --git a/v2rayng/hysteria/extras/sniff/internal/quic/LICENSE b/v2rayng/hysteria/extras/sniff/internal/quic/LICENSE new file mode 100644 index 0000000000..43970c4103 --- /dev/null +++ b/v2rayng/hysteria/extras/sniff/internal/quic/LICENSE @@ -0,0 +1,31 @@ +Author:: Cuong Manh Le +Copyright:: Copyright (c) 2023, Cuong Manh Le +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the @organization@ nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL LE MANH CUONG +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/v2rayng/hysteria/extras/sniff/internal/quic/README.md b/v2rayng/hysteria/extras/sniff/internal/quic/README.md new file mode 100644 index 0000000000..8f3a5e2a66 --- /dev/null +++ b/v2rayng/hysteria/extras/sniff/internal/quic/README.md @@ -0,0 +1 @@ +The code here is from https://github.com/cuonglm/quicsni with various modifications. \ No newline at end of file diff --git a/v2rayng/hysteria/extras/sniff/internal/quic/header.go b/v2rayng/hysteria/extras/sniff/internal/quic/header.go new file mode 100644 index 0000000000..c1a5e7c4c8 --- /dev/null +++ b/v2rayng/hysteria/extras/sniff/internal/quic/header.go @@ -0,0 +1,105 @@ +package quic + +import ( + "bytes" + "encoding/binary" + "errors" + "io" + + "github.com/apernet/quic-go/quicvarint" +) + +// The Header represents a QUIC header. +type Header struct { + Type uint8 + Version uint32 + SrcConnectionID []byte + DestConnectionID []byte + Length int64 + Token []byte +} + +// ParseInitialHeader parses the initial packet of a QUIC connection, +// return the initial header and number of bytes read so far. +func ParseInitialHeader(data []byte) (*Header, int64, error) { + br := bytes.NewReader(data) + hdr, err := parseLongHeader(br) + if err != nil { + return nil, 0, err + } + n := int64(len(data) - br.Len()) + return hdr, n, nil +} + +func parseLongHeader(b *bytes.Reader) (*Header, error) { + typeByte, err := b.ReadByte() + if err != nil { + return nil, err + } + h := &Header{} + ver, err := beUint32(b) + if err != nil { + return nil, err + } + h.Version = ver + if h.Version != 0 && typeByte&0x40 == 0 { + return nil, errors.New("not a QUIC packet") + } + destConnIDLen, err := b.ReadByte() + if err != nil { + return nil, err + } + h.DestConnectionID = make([]byte, int(destConnIDLen)) + if err := readConnectionID(b, h.DestConnectionID); err != nil { + return nil, err + } + srcConnIDLen, err := b.ReadByte() + if err != nil { + return nil, err + } + h.SrcConnectionID = make([]byte, int(srcConnIDLen)) + if err := readConnectionID(b, h.SrcConnectionID); err != nil { + return nil, err + } + + initialPacketType := byte(0b00) + if h.Version == V2 { + initialPacketType = 0b01 + } + if (typeByte >> 4 & 0b11) == initialPacketType { + tokenLen, err := quicvarint.Read(b) + if err != nil { + return nil, err + } + if tokenLen > uint64(b.Len()) { + return nil, io.EOF + } + h.Token = make([]byte, tokenLen) + if _, err := io.ReadFull(b, h.Token); err != nil { + return nil, err + } + } + + pl, err := quicvarint.Read(b) + if err != nil { + return nil, err + } + h.Length = int64(pl) + return h, err +} + +func readConnectionID(r io.Reader, cid []byte) error { + _, err := io.ReadFull(r, cid) + if err == io.ErrUnexpectedEOF { + return io.EOF + } + return nil +} + +func beUint32(r io.Reader) (uint32, error) { + b := make([]byte, 4) + if _, err := io.ReadFull(r, b); err != nil { + return 0, err + } + return binary.BigEndian.Uint32(b), nil +} diff --git a/v2rayng/hysteria/extras/sniff/internal/quic/packet_protector.go b/v2rayng/hysteria/extras/sniff/internal/quic/packet_protector.go new file mode 100644 index 0000000000..42de841134 --- /dev/null +++ b/v2rayng/hysteria/extras/sniff/internal/quic/packet_protector.go @@ -0,0 +1,193 @@ +package quic + +import ( + "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "crypto/tls" + "encoding/binary" + "errors" + "fmt" + "hash" + + "golang.org/x/crypto/chacha20" + "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/cryptobyte" + "golang.org/x/crypto/hkdf" +) + +// NewProtectionKey creates a new ProtectionKey. +func NewProtectionKey(suite uint16, secret []byte, v uint32) (*ProtectionKey, error) { + return newProtectionKey(suite, secret, v) +} + +// NewInitialProtectionKey is like NewProtectionKey, but the returned protection key +// is used for encrypt/decrypt Initial Packet only. +// +// See: https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-initial-secrets +func NewInitialProtectionKey(secret []byte, v uint32) (*ProtectionKey, error) { + return NewProtectionKey(tls.TLS_AES_128_GCM_SHA256, secret, v) +} + +// NewPacketProtector creates a new PacketProtector. +func NewPacketProtector(key *ProtectionKey) *PacketProtector { + return &PacketProtector{key: key} +} + +// PacketProtector is used for protecting a QUIC packet. +// +// See: https://www.rfc-editor.org/rfc/rfc9001.html#name-packet-protection +type PacketProtector struct { + key *ProtectionKey +} + +// UnProtect decrypts a QUIC packet. +func (pp *PacketProtector) UnProtect(packet []byte, pnOffset, pnMax int64) ([]byte, error) { + if isLongHeader(packet[0]) && int64(len(packet)) < pnOffset+4+16 { + return nil, errors.New("packet with long header is too small") + } + + // https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-sample + sampleOffset := pnOffset + 4 + sample := packet[sampleOffset : sampleOffset+16] + + // https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-applicati + mask := pp.key.headerProtection(sample) + if isLongHeader(packet[0]) { + // Long header: 4 bits masked + packet[0] ^= mask[0] & 0x0f + } else { + // Short header: 5 bits masked + packet[0] ^= mask[0] & 0x1f + } + + pnLen := packet[0]&0x3 + 1 + pn := int64(0) + for i := uint8(0); i < pnLen; i++ { + packet[pnOffset:][i] ^= mask[1+i] + pn = (pn << 8) | int64(packet[pnOffset:][i]) + } + pn = decodePacketNumber(pnMax, pn, pnLen) + hdr := packet[:pnOffset+int64(pnLen)] + payload := packet[pnOffset:][pnLen:] + dec, err := pp.key.aead.Open(payload[:0], pp.key.nonce(pn), payload, hdr) + if err != nil { + return nil, fmt.Errorf("decryption failed: %w", err) + } + return dec, nil +} + +// ProtectionKey is the key used to protect a QUIC packet. +type ProtectionKey struct { + aead cipher.AEAD + headerProtection func(sample []byte) (mask []byte) + iv []byte +} + +// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-aead-usage +// +// "The 62 bits of the reconstructed QUIC packet number in network byte order are +// left-padded with zeros to the size of the IV. The exclusive OR of the padded +// packet number and the IV forms the AEAD nonce." +func (pk *ProtectionKey) nonce(pn int64) []byte { + nonce := make([]byte, len(pk.iv)) + binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(pn)) + for i := range pk.iv { + nonce[i] ^= pk.iv[i] + } + return nonce +} + +func newProtectionKey(suite uint16, secret []byte, v uint32) (*ProtectionKey, error) { + switch suite { + case tls.TLS_AES_128_GCM_SHA256: + key := hkdfExpandLabel(crypto.SHA256.New, secret, keyLabel(v), nil, 16) + c, err := aes.NewCipher(key) + if err != nil { + panic(err) + } + aead, err := cipher.NewGCM(c) + if err != nil { + panic(err) + } + iv := hkdfExpandLabel(crypto.SHA256.New, secret, ivLabel(v), nil, aead.NonceSize()) + hpKey := hkdfExpandLabel(crypto.SHA256.New, secret, headerProtectionLabel(v), nil, 16) + hp, err := aes.NewCipher(hpKey) + if err != nil { + panic(err) + } + k := &ProtectionKey{} + k.aead = aead + // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-aes-based-header-protection + k.headerProtection = func(sample []byte) []byte { + mask := make([]byte, hp.BlockSize()) + hp.Encrypt(mask, sample) + return mask + } + k.iv = iv + return k, nil + case tls.TLS_CHACHA20_POLY1305_SHA256: + key := hkdfExpandLabel(crypto.SHA256.New, secret, keyLabel(v), nil, chacha20poly1305.KeySize) + aead, err := chacha20poly1305.New(key) + if err != nil { + return nil, err + } + iv := hkdfExpandLabel(crypto.SHA256.New, secret, ivLabel(v), nil, aead.NonceSize()) + hpKey := hkdfExpandLabel(sha256.New, secret, headerProtectionLabel(v), nil, chacha20.KeySize) + k := &ProtectionKey{} + k.aead = aead + // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-chacha20-based-header-prote + k.headerProtection = func(sample []byte) []byte { + nonce := sample[4:16] + c, err := chacha20.NewUnauthenticatedCipher(hpKey, nonce) + if err != nil { + panic(err) + } + c.SetCounter(binary.LittleEndian.Uint32(sample[:4])) + mask := make([]byte, 5) + c.XORKeyStream(mask, mask) + return mask + } + k.iv = iv + return k, nil + } + return nil, errors.New("not supported cipher suite") +} + +// decodePacketNumber decode the packet number after header protection removed. +// +// See: https://datatracker.ietf.org/doc/html/draft-ietf-quic-transport-32#section-appendix.a +func decodePacketNumber(largest, truncated int64, nbits uint8) int64 { + expected := largest + 1 + win := int64(1 << (nbits * 8)) + hwin := win / 2 + mask := win - 1 + candidate := (expected &^ mask) | truncated + switch { + case candidate <= expected-hwin && candidate < (1<<62)-win: + return candidate + win + case candidate > expected+hwin && candidate >= win: + return candidate - win + } + return candidate +} + +// Copied from crypto/tls/key_schedule.go. +func hkdfExpandLabel(hash func() hash.Hash, secret []byte, label string, context []byte, length int) []byte { + var hkdfLabel cryptobyte.Builder + hkdfLabel.AddUint16(uint16(length)) + hkdfLabel.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes([]byte("tls13 ")) + b.AddBytes([]byte(label)) + }) + hkdfLabel.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(context) + }) + out := make([]byte, length) + n, err := hkdf.Expand(hash, secret, hkdfLabel.BytesOrPanic()).Read(out) + if err != nil || n != length { + panic("quic: HKDF-Expand-Label invocation failed unexpectedly") + } + return out +} diff --git a/v2rayng/hysteria/extras/sniff/internal/quic/packet_protector_test.go b/v2rayng/hysteria/extras/sniff/internal/quic/packet_protector_test.go new file mode 100644 index 0000000000..bc355d2186 --- /dev/null +++ b/v2rayng/hysteria/extras/sniff/internal/quic/packet_protector_test.go @@ -0,0 +1,94 @@ +package quic + +import ( + "bytes" + "crypto" + "crypto/tls" + "encoding/hex" + "strings" + "testing" + "unicode" + + "golang.org/x/crypto/hkdf" +) + +func TestInitialPacketProtector_UnProtect(t *testing.T) { + // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-server-initial + protect := mustHexDecodeString(` + c7ff0000200008f067a5502a4262b500 4075fb12ff07823a5d24534d906ce4c7 + 6782a2167e3479c0f7f6395dc2c91676 302fe6d70bb7cbeb117b4ddb7d173498 + 44fd61dae200b8338e1b932976b61d91 e64a02e9e0ee72e3a6f63aba4ceeeec5 + be2f24f2d86027572943533846caa13e 6f163fb257473d0eda5047360fd4a47e + fd8142fafc0f76 + `) + unProtect := mustHexDecodeString(` + 02000000000600405a020000560303ee fce7f7b37ba1d1632e96677825ddf739 + 88cfc79825df566dc5430b9a045a1200 130100002e00330024001d00209d3c94 + 0d89690b84d08a60993c144eca684d10 81287c834d5311bcf32bb9da1a002b00 + 020304 + `) + + connID := mustHexDecodeString(`8394c8f03e515708`) + + packet := append([]byte{}, protect...) + hdr, offset, err := ParseInitialHeader(packet) + if err != nil { + t.Fatal(err) + } + + initialSecret := hkdf.Extract(crypto.SHA256.New, connID, getSalt(hdr.Version)) + serverSecret := hkdfExpandLabel(crypto.SHA256.New, initialSecret, "server in", []byte{}, crypto.SHA256.Size()) + key, err := NewInitialProtectionKey(serverSecret, hdr.Version) + if err != nil { + t.Fatal(err) + } + pp := NewPacketProtector(key) + got, err := pp.UnProtect(protect, offset, 1) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, unProtect) { + t.Error("UnProtect returns wrong result") + } +} + +func TestPacketProtectorShortHeader_UnProtect(t *testing.T) { + // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-chacha20-poly1305-short-hea + protect := mustHexDecodeString(`4cfe4189655e5cd55c41f69080575d7999c25a5bfb`) + unProtect := mustHexDecodeString(`01`) + hdr := mustHexDecodeString(`4200bff4`) + + secret := mustHexDecodeString(`9ac312a7f877468ebe69422748ad00a1 5443f18203a07d6060f688f30f21632b`) + k, err := NewProtectionKey(tls.TLS_CHACHA20_POLY1305_SHA256, secret, V1) + if err != nil { + t.Fatal(err) + } + + pnLen := int(hdr[0]&0x03) + 1 + offset := len(hdr) - pnLen + pp := NewPacketProtector(k) + got, err := pp.UnProtect(protect, int64(offset), 654360564) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, unProtect) { + t.Error("UnProtect returns wrong result") + } +} + +func mustHexDecodeString(s string) []byte { + b, err := hex.DecodeString(normalizeHex(s)) + if err != nil { + panic(err) + } + return b +} + +func normalizeHex(s string) string { + return strings.Map(func(c rune) rune { + if unicode.IsSpace(c) { + return -1 + } + return c + }, s) +} diff --git a/v2rayng/hysteria/extras/sniff/internal/quic/payload.go b/v2rayng/hysteria/extras/sniff/internal/quic/payload.go new file mode 100644 index 0000000000..453b714a4c --- /dev/null +++ b/v2rayng/hysteria/extras/sniff/internal/quic/payload.go @@ -0,0 +1,122 @@ +package quic + +import ( + "bytes" + "crypto" + "errors" + "fmt" + "io" + "sort" + + "github.com/apernet/quic-go/quicvarint" + "golang.org/x/crypto/hkdf" +) + +func ReadCryptoPayload(packet []byte) ([]byte, error) { + hdr, offset, err := ParseInitialHeader(packet) + if err != nil { + return nil, err + } + // Some sanity checks + if hdr.Version != V1 && hdr.Version != V2 { + return nil, fmt.Errorf("unsupported version: %x", hdr.Version) + } + if offset == 0 || hdr.Length == 0 { + return nil, errors.New("invalid packet") + } + + initialSecret := hkdf.Extract(crypto.SHA256.New, hdr.DestConnectionID, getSalt(hdr.Version)) + clientSecret := hkdfExpandLabel(crypto.SHA256.New, initialSecret, "client in", []byte{}, crypto.SHA256.Size()) + key, err := NewInitialProtectionKey(clientSecret, hdr.Version) + if err != nil { + return nil, fmt.Errorf("NewInitialProtectionKey: %w", err) + } + pp := NewPacketProtector(key) + // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-client-initial + // + // "The unprotected header includes the connection ID and a 4-byte packet number encoding for a packet number of 2" + if int64(len(packet)) < offset+hdr.Length { + return nil, fmt.Errorf("packet is too short: %d < %d", len(packet), offset+hdr.Length) + } + unProtectedPayload, err := pp.UnProtect(packet[:offset+hdr.Length], offset, 2) + if err != nil { + return nil, err + } + frs, err := extractCryptoFrames(bytes.NewReader(unProtectedPayload)) + if err != nil { + return nil, err + } + data := assembleCryptoFrames(frs) + if data == nil { + return nil, errors.New("unable to assemble crypto frames") + } + return data, nil +} + +const ( + paddingFrameType = 0x00 + pingFrameType = 0x01 + cryptoFrameType = 0x06 +) + +type cryptoFrame struct { + Offset int64 + Data []byte +} + +func extractCryptoFrames(r *bytes.Reader) ([]cryptoFrame, error) { + var frames []cryptoFrame + for r.Len() > 0 { + typ, err := quicvarint.Read(r) + if err != nil { + return nil, err + } + if typ == paddingFrameType || typ == pingFrameType { + continue + } + if typ != cryptoFrameType { + return nil, fmt.Errorf("encountered unexpected frame type: %d", typ) + } + var frame cryptoFrame + offset, err := quicvarint.Read(r) + if err != nil { + return nil, err + } + frame.Offset = int64(offset) + dataLen, err := quicvarint.Read(r) + if err != nil { + return nil, err + } + frame.Data = make([]byte, dataLen) + if _, err := io.ReadFull(r, frame.Data); err != nil { + return nil, err + } + frames = append(frames, frame) + } + return frames, nil +} + +// assembleCryptoFrames assembles multiple crypto frames into a single slice (if possible). +// It returns an error if the frames cannot be assembled. This can happen if the frames are not contiguous. +func assembleCryptoFrames(frames []cryptoFrame) []byte { + if len(frames) == 0 { + return nil + } + if len(frames) == 1 { + return frames[0].Data + } + // sort the frames by offset + sort.Slice(frames, func(i, j int) bool { return frames[i].Offset < frames[j].Offset }) + // check if the frames are contiguous + for i := 1; i < len(frames); i++ { + if frames[i].Offset != frames[i-1].Offset+int64(len(frames[i-1].Data)) { + return nil + } + } + // concatenate the frames + data := make([]byte, frames[len(frames)-1].Offset+int64(len(frames[len(frames)-1].Data))) + for _, frame := range frames { + copy(data[frame.Offset:], frame.Data) + } + return data +} diff --git a/v2rayng/hysteria/extras/sniff/internal/quic/quic.go b/v2rayng/hysteria/extras/sniff/internal/quic/quic.go new file mode 100644 index 0000000000..1cfa103867 --- /dev/null +++ b/v2rayng/hysteria/extras/sniff/internal/quic/quic.go @@ -0,0 +1,59 @@ +package quic + +const ( + V1 uint32 = 0x1 + V2 uint32 = 0x6b3343cf + + hkdfLabelKeyV1 = "quic key" + hkdfLabelKeyV2 = "quicv2 key" + hkdfLabelIVV1 = "quic iv" + hkdfLabelIVV2 = "quicv2 iv" + hkdfLabelHPV1 = "quic hp" + hkdfLabelHPV2 = "quicv2 hp" +) + +var ( + quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99} + // https://www.rfc-editor.org/rfc/rfc9001.html#name-initial-secrets + quicSaltV1 = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a} + // https://www.ietf.org/archive/id/draft-ietf-quic-v2-10.html#name-initial-salt-2 + quicSaltV2 = []byte{0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9} +) + +// isLongHeader reports whether b is the first byte of a long header packet. +func isLongHeader(b byte) bool { + return b&0x80 > 0 +} + +func getSalt(v uint32) []byte { + switch v { + case V1: + return quicSaltV1 + case V2: + return quicSaltV2 + } + return quicSaltOld +} + +func keyLabel(v uint32) string { + kl := hkdfLabelKeyV1 + if v == V2 { + kl = hkdfLabelKeyV2 + } + return kl +} + +func ivLabel(v uint32) string { + ivl := hkdfLabelIVV1 + if v == V2 { + ivl = hkdfLabelIVV2 + } + return ivl +} + +func headerProtectionLabel(v uint32) string { + if v == V2 { + return hkdfLabelHPV2 + } + return hkdfLabelHPV1 +} diff --git a/v2rayng/hysteria/extras/sniff/mock_Stream.go b/v2rayng/hysteria/extras/sniff/mock_Stream.go new file mode 100644 index 0000000000..8b21e953b9 --- /dev/null +++ b/v2rayng/hysteria/extras/sniff/mock_Stream.go @@ -0,0 +1,492 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package sniff + +import ( + context "context" + + qerr "github.com/apernet/quic-go" + mock "github.com/stretchr/testify/mock" + + time "time" +) + +// mockStream is an autogenerated mock type for the Stream type +type mockStream struct { + mock.Mock +} + +type mockStream_Expecter struct { + mock *mock.Mock +} + +func (_m *mockStream) EXPECT() *mockStream_Expecter { + return &mockStream_Expecter{mock: &_m.Mock} +} + +// CancelRead provides a mock function with given fields: _a0 +func (_m *mockStream) CancelRead(_a0 qerr.StreamErrorCode) { + _m.Called(_a0) +} + +// mockStream_CancelRead_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CancelRead' +type mockStream_CancelRead_Call struct { + *mock.Call +} + +// CancelRead is a helper method to define mock.On call +// - _a0 qerr.StreamErrorCode +func (_e *mockStream_Expecter) CancelRead(_a0 interface{}) *mockStream_CancelRead_Call { + return &mockStream_CancelRead_Call{Call: _e.mock.On("CancelRead", _a0)} +} + +func (_c *mockStream_CancelRead_Call) Run(run func(_a0 qerr.StreamErrorCode)) *mockStream_CancelRead_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(qerr.StreamErrorCode)) + }) + return _c +} + +func (_c *mockStream_CancelRead_Call) Return() *mockStream_CancelRead_Call { + _c.Call.Return() + return _c +} + +func (_c *mockStream_CancelRead_Call) RunAndReturn(run func(qerr.StreamErrorCode)) *mockStream_CancelRead_Call { + _c.Call.Return(run) + return _c +} + +// CancelWrite provides a mock function with given fields: _a0 +func (_m *mockStream) CancelWrite(_a0 qerr.StreamErrorCode) { + _m.Called(_a0) +} + +// mockStream_CancelWrite_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CancelWrite' +type mockStream_CancelWrite_Call struct { + *mock.Call +} + +// CancelWrite is a helper method to define mock.On call +// - _a0 qerr.StreamErrorCode +func (_e *mockStream_Expecter) CancelWrite(_a0 interface{}) *mockStream_CancelWrite_Call { + return &mockStream_CancelWrite_Call{Call: _e.mock.On("CancelWrite", _a0)} +} + +func (_c *mockStream_CancelWrite_Call) Run(run func(_a0 qerr.StreamErrorCode)) *mockStream_CancelWrite_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(qerr.StreamErrorCode)) + }) + return _c +} + +func (_c *mockStream_CancelWrite_Call) Return() *mockStream_CancelWrite_Call { + _c.Call.Return() + return _c +} + +func (_c *mockStream_CancelWrite_Call) RunAndReturn(run func(qerr.StreamErrorCode)) *mockStream_CancelWrite_Call { + _c.Call.Return(run) + return _c +} + +// Close provides a mock function with given fields: +func (_m *mockStream) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockStream_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type mockStream_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *mockStream_Expecter) Close() *mockStream_Close_Call { + return &mockStream_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *mockStream_Close_Call) Run(run func()) *mockStream_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *mockStream_Close_Call) Return(_a0 error) *mockStream_Close_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockStream_Close_Call) RunAndReturn(run func() error) *mockStream_Close_Call { + _c.Call.Return(run) + return _c +} + +// Context provides a mock function with given fields: +func (_m *mockStream) Context() context.Context { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Context") + } + + var r0 context.Context + if rf, ok := ret.Get(0).(func() context.Context); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(context.Context) + } + } + + return r0 +} + +// mockStream_Context_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Context' +type mockStream_Context_Call struct { + *mock.Call +} + +// Context is a helper method to define mock.On call +func (_e *mockStream_Expecter) Context() *mockStream_Context_Call { + return &mockStream_Context_Call{Call: _e.mock.On("Context")} +} + +func (_c *mockStream_Context_Call) Run(run func()) *mockStream_Context_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *mockStream_Context_Call) Return(_a0 context.Context) *mockStream_Context_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockStream_Context_Call) RunAndReturn(run func() context.Context) *mockStream_Context_Call { + _c.Call.Return(run) + return _c +} + +// Read provides a mock function with given fields: p +func (_m *mockStream) Read(p []byte) (int, error) { + ret := _m.Called(p) + + if len(ret) == 0 { + panic("no return value specified for Read") + } + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok { + return rf(p) + } + if rf, ok := ret.Get(0).(func([]byte) int); ok { + r0 = rf(p) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(p) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockStream_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read' +type mockStream_Read_Call struct { + *mock.Call +} + +// Read is a helper method to define mock.On call +// - p []byte +func (_e *mockStream_Expecter) Read(p interface{}) *mockStream_Read_Call { + return &mockStream_Read_Call{Call: _e.mock.On("Read", p)} +} + +func (_c *mockStream_Read_Call) Run(run func(p []byte)) *mockStream_Read_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte)) + }) + return _c +} + +func (_c *mockStream_Read_Call) Return(n int, err error) *mockStream_Read_Call { + _c.Call.Return(n, err) + return _c +} + +func (_c *mockStream_Read_Call) RunAndReturn(run func([]byte) (int, error)) *mockStream_Read_Call { + _c.Call.Return(run) + return _c +} + +// SetDeadline provides a mock function with given fields: t +func (_m *mockStream) SetDeadline(t time.Time) error { + ret := _m.Called(t) + + if len(ret) == 0 { + panic("no return value specified for SetDeadline") + } + + var r0 error + if rf, ok := ret.Get(0).(func(time.Time) error); ok { + r0 = rf(t) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockStream_SetDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDeadline' +type mockStream_SetDeadline_Call struct { + *mock.Call +} + +// SetDeadline is a helper method to define mock.On call +// - t time.Time +func (_e *mockStream_Expecter) SetDeadline(t interface{}) *mockStream_SetDeadline_Call { + return &mockStream_SetDeadline_Call{Call: _e.mock.On("SetDeadline", t)} +} + +func (_c *mockStream_SetDeadline_Call) Run(run func(t time.Time)) *mockStream_SetDeadline_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Time)) + }) + return _c +} + +func (_c *mockStream_SetDeadline_Call) Return(_a0 error) *mockStream_SetDeadline_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockStream_SetDeadline_Call) RunAndReturn(run func(time.Time) error) *mockStream_SetDeadline_Call { + _c.Call.Return(run) + return _c +} + +// SetReadDeadline provides a mock function with given fields: t +func (_m *mockStream) SetReadDeadline(t time.Time) error { + ret := _m.Called(t) + + if len(ret) == 0 { + panic("no return value specified for SetReadDeadline") + } + + var r0 error + if rf, ok := ret.Get(0).(func(time.Time) error); ok { + r0 = rf(t) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockStream_SetReadDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetReadDeadline' +type mockStream_SetReadDeadline_Call struct { + *mock.Call +} + +// SetReadDeadline is a helper method to define mock.On call +// - t time.Time +func (_e *mockStream_Expecter) SetReadDeadline(t interface{}) *mockStream_SetReadDeadline_Call { + return &mockStream_SetReadDeadline_Call{Call: _e.mock.On("SetReadDeadline", t)} +} + +func (_c *mockStream_SetReadDeadline_Call) Run(run func(t time.Time)) *mockStream_SetReadDeadline_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Time)) + }) + return _c +} + +func (_c *mockStream_SetReadDeadline_Call) Return(_a0 error) *mockStream_SetReadDeadline_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockStream_SetReadDeadline_Call) RunAndReturn(run func(time.Time) error) *mockStream_SetReadDeadline_Call { + _c.Call.Return(run) + return _c +} + +// SetWriteDeadline provides a mock function with given fields: t +func (_m *mockStream) SetWriteDeadline(t time.Time) error { + ret := _m.Called(t) + + if len(ret) == 0 { + panic("no return value specified for SetWriteDeadline") + } + + var r0 error + if rf, ok := ret.Get(0).(func(time.Time) error); ok { + r0 = rf(t) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockStream_SetWriteDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWriteDeadline' +type mockStream_SetWriteDeadline_Call struct { + *mock.Call +} + +// SetWriteDeadline is a helper method to define mock.On call +// - t time.Time +func (_e *mockStream_Expecter) SetWriteDeadline(t interface{}) *mockStream_SetWriteDeadline_Call { + return &mockStream_SetWriteDeadline_Call{Call: _e.mock.On("SetWriteDeadline", t)} +} + +func (_c *mockStream_SetWriteDeadline_Call) Run(run func(t time.Time)) *mockStream_SetWriteDeadline_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Time)) + }) + return _c +} + +func (_c *mockStream_SetWriteDeadline_Call) Return(_a0 error) *mockStream_SetWriteDeadline_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockStream_SetWriteDeadline_Call) RunAndReturn(run func(time.Time) error) *mockStream_SetWriteDeadline_Call { + _c.Call.Return(run) + return _c +} + +// StreamID provides a mock function with given fields: +func (_m *mockStream) StreamID() qerr.StreamID { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for StreamID") + } + + var r0 qerr.StreamID + if rf, ok := ret.Get(0).(func() qerr.StreamID); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(qerr.StreamID) + } + + return r0 +} + +// mockStream_StreamID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StreamID' +type mockStream_StreamID_Call struct { + *mock.Call +} + +// StreamID is a helper method to define mock.On call +func (_e *mockStream_Expecter) StreamID() *mockStream_StreamID_Call { + return &mockStream_StreamID_Call{Call: _e.mock.On("StreamID")} +} + +func (_c *mockStream_StreamID_Call) Run(run func()) *mockStream_StreamID_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *mockStream_StreamID_Call) Return(_a0 qerr.StreamID) *mockStream_StreamID_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockStream_StreamID_Call) RunAndReturn(run func() qerr.StreamID) *mockStream_StreamID_Call { + _c.Call.Return(run) + return _c +} + +// Write provides a mock function with given fields: p +func (_m *mockStream) Write(p []byte) (int, error) { + ret := _m.Called(p) + + if len(ret) == 0 { + panic("no return value specified for Write") + } + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok { + return rf(p) + } + if rf, ok := ret.Get(0).(func([]byte) int); ok { + r0 = rf(p) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(p) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockStream_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write' +type mockStream_Write_Call struct { + *mock.Call +} + +// Write is a helper method to define mock.On call +// - p []byte +func (_e *mockStream_Expecter) Write(p interface{}) *mockStream_Write_Call { + return &mockStream_Write_Call{Call: _e.mock.On("Write", p)} +} + +func (_c *mockStream_Write_Call) Run(run func(p []byte)) *mockStream_Write_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte)) + }) + return _c +} + +func (_c *mockStream_Write_Call) Return(n int, err error) *mockStream_Write_Call { + _c.Call.Return(n, err) + return _c +} + +func (_c *mockStream_Write_Call) RunAndReturn(run func([]byte) (int, error)) *mockStream_Write_Call { + _c.Call.Return(run) + return _c +} + +// newMockStream creates a new instance of mockStream. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockStream(t interface { + mock.TestingT + Cleanup(func()) +}) *mockStream { + mock := &mockStream{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/v2rayng/hysteria/extras/sniff/sniff.go b/v2rayng/hysteria/extras/sniff/sniff.go new file mode 100644 index 0000000000..9994b8a4c0 --- /dev/null +++ b/v2rayng/hysteria/extras/sniff/sniff.go @@ -0,0 +1,199 @@ +package sniff + +import ( + "bufio" + "io" + "net" + "net/http" + "strconv" + "strings" + "time" + + "github.com/apernet/quic-go" + utls "github.com/refraction-networking/utls" + + "github.com/apernet/hysteria/core/v2/server" + quicInternal "github.com/apernet/hysteria/extras/v2/sniff/internal/quic" + "github.com/apernet/hysteria/extras/v2/utils" +) + +const ( + sniffDefaultTimeout = 4 * time.Second +) + +var _ server.RequestHook = (*Sniffer)(nil) + +// Sniffer is a server core RequestHook that performs packet inspection and possibly +// rewrites the request address based on what's in the protocol header. +// This is mainly for inbounds that inherently cannot get domain information (e.g. TUN), +// in which case sniffing can restore the domains and apply ACLs correctly. +// Currently supports HTTP, HTTPS (TLS) and QUIC. +type Sniffer struct { + Timeout time.Duration + RewriteDomain bool // Whether to rewrite the address even when it's already a domain + TCPPorts utils.PortUnion + UDPPorts utils.PortUnion +} + +func (h *Sniffer) isDomain(addr string) bool { + host, _, err := net.SplitHostPort(addr) + if err != nil { + return false + } + return net.ParseIP(host) == nil +} + +func (h *Sniffer) isHTTP(buf []byte) bool { + if len(buf) < 3 { + return false + } + // First 3 bytes should be English letters (whatever HTTP method) + for _, b := range buf[:3] { + if (b < 'A' || b > 'Z') && (b < 'a' || b > 'z') { + return false + } + } + return true +} + +func (h *Sniffer) isTLS(buf []byte) bool { + if len(buf) < 3 { + return false + } + return buf[0] >= 0x16 && buf[0] <= 0x17 && + buf[1] == 0x03 && buf[2] <= 0x09 +} + +func (h *Sniffer) Check(isUDP bool, reqAddr string) bool { + // @ means it's internal (e.g. speed test) + if strings.HasPrefix(reqAddr, "@") { + return false + } + host, port, err := net.SplitHostPort(reqAddr) + if err != nil { + return false + } + if !h.RewriteDomain && net.ParseIP(host) == nil { + // Is a domain and domain rewriting is disabled + return false + } + portNum, err := strconv.Atoi(port) + if err != nil { + return false + } + if isUDP { + return h.UDPPorts == nil || h.UDPPorts.Contains(uint16(portNum)) + } else { + return h.TCPPorts == nil || h.TCPPorts.Contains(uint16(portNum)) + } +} + +func (h *Sniffer) TCP(stream quic.Stream, reqAddr *string) ([]byte, error) { + var err error + if h.Timeout == 0 { + err = stream.SetReadDeadline(time.Now().Add(sniffDefaultTimeout)) + } else { + err = stream.SetReadDeadline(time.Now().Add(h.Timeout)) + } + if err != nil { + return nil, err + } + // Make sure to reset the deadline after sniffing + defer stream.SetReadDeadline(time.Time{}) + // Read 3 bytes to determine the protocol + pre := make([]byte, 3) + n, err := io.ReadFull(stream, pre) + if err != nil { + // Not enough within the timeout, just return what we have + return pre[:n], nil + } + if h.isHTTP(pre) { + // HTTP + tr := &teeReader{Stream: stream, Pre: pre} + req, _ := http.ReadRequest(bufio.NewReader(tr)) + if req != nil && req.Host != "" { + // req.Host can be host:port, in which case we need to extract the host part + host, _, err := net.SplitHostPort(req.Host) + if err != nil { + // No port, just use the whole string + host = req.Host + } + _, port, err := net.SplitHostPort(*reqAddr) + if err != nil { + return nil, err + } + *reqAddr = net.JoinHostPort(host, port) + } + return tr.Buffer(), nil + } else if h.isTLS(pre) { + // TLS + // Need to read 2 more bytes (content length) + pre = append(pre, make([]byte, 2)...) + n, err = io.ReadFull(stream, pre[3:]) + if err != nil { + // Not enough within the timeout, just return what we have + return pre[:3+n], nil + } + contentLength := int(pre[3])<<8 | int(pre[4]) + pre = append(pre, make([]byte, contentLength)...) + n, err = io.ReadFull(stream, pre[5:]) + if err != nil { + // Not enough within the timeout, just return what we have + return pre[:5+n], nil + } + clientHello := utls.UnmarshalClientHello(pre[5:]) + if clientHello != nil && clientHello.ServerName != "" { + _, port, err := net.SplitHostPort(*reqAddr) + if err != nil { + return nil, err + } + *reqAddr = net.JoinHostPort(clientHello.ServerName, port) + } + return pre, nil + } else { + // Unrecognized protocol, just return what we have + return pre, nil + } +} + +func (h *Sniffer) UDP(data []byte, reqAddr *string) error { + pl, err := quicInternal.ReadCryptoPayload(data) + if err != nil || len(pl) < 4 || pl[0] != 0x01 { + // Unrecognized protocol, incomplete payload or not a client hello + return nil + } + clientHello := utls.UnmarshalClientHello(pl) + if clientHello != nil && clientHello.ServerName != "" { + _, port, err := net.SplitHostPort(*reqAddr) + if err != nil { + return err + } + *reqAddr = net.JoinHostPort(clientHello.ServerName, port) + } + return nil +} + +type teeReader struct { + Stream quic.Stream + Pre []byte + + buf []byte +} + +func (c *teeReader) Read(b []byte) (n int, err error) { + if len(c.Pre) > 0 { + n = copy(b, c.Pre) + c.Pre = c.Pre[n:] + c.buf = append(c.buf, b[:n]...) + return n, nil + } + n, err = c.Stream.Read(b) + if n > 0 { + c.buf = append(c.buf, b[:n]...) + } + return n, err +} + +func (c *teeReader) Buffer() []byte { + return append(c.Pre, c.buf...) +} diff --git a/v2rayng/hysteria/extras/sniff/sniff_test.go b/v2rayng/hysteria/extras/sniff/sniff_test.go new file mode 100644 index 0000000000..445660bb0b --- /dev/null +++ b/v2rayng/hysteria/extras/sniff/sniff_test.go @@ -0,0 +1,147 @@ +package sniff + +import ( + "encoding/base64" + "io" + "testing" + "time" + + "github.com/apernet/hysteria/extras/v2/utils" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestSnifferCheck(t *testing.T) { + sniffer := &Sniffer{ + Timeout: 1 * time.Second, + RewriteDomain: false, + TCPPorts: nil, // nil = all + UDPPorts: nil, // nil = all + } + + assert.True(t, sniffer.Check(false, "1.1.1.1:80")) + assert.False(t, sniffer.Check(false, "example.com:443")) + + sniffer.RewriteDomain = true + assert.True(t, sniffer.Check(false, "example.com:443")) + + sniffer.TCPPorts = []utils.PortRange{{80, 80}} + assert.True(t, sniffer.Check(false, "google.com:80")) + assert.False(t, sniffer.Check(false, "google.com:443")) + + sniffer.UDPPorts = []utils.PortRange{{443, 443}} + assert.True(t, sniffer.Check(true, "google.com:443")) + assert.False(t, sniffer.Check(true, "google.com:80")) +} + +func TestSnifferTCP(t *testing.T) { + sniffer := &Sniffer{ + Timeout: 1 * time.Second, + RewriteDomain: false, + } + + buf := &[]byte{} + + // Test HTTP + *buf = []byte("POST /hello HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "User-Agent: mamamiya\r\n" + + "Content-Length: 27\r\n" + + "Connection: keep-alive\r\n\r\n" + + "param1=value1¶m2=value2") + index := 0 + stream := &mockStream{} + stream.EXPECT().SetReadDeadline(mock.Anything).Return(nil) + stream.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) { + if index < len(*buf) { + n := copy(bs, (*buf)[index:]) + index += n + return n, nil + } else { + return 0, io.EOF + } + }) + + // Rewrite IP to domain + reqAddr := "111.111.111.111:80" + putback, err := sniffer.TCP(stream, &reqAddr) + assert.NoError(t, err) + assert.Equal(t, *buf, putback) + assert.Equal(t, "example.com:80", reqAddr) + + // Test HTTP with Host as host:port + *buf = []byte("GET / HTTP/1.1\r\n" + + "Host: example.com:8080\r\n" + + "User-Agent: test-agent\r\n" + + "Accept: */*\r\n\r\n") + index = 0 + reqAddr = "222.222.222.222:10086" + putback, err = sniffer.TCP(stream, &reqAddr) + assert.NoError(t, err) + assert.Equal(t, *buf, putback) + assert.Equal(t, "example.com:10086", reqAddr) + + // Test TLS + *buf, err = base64.StdEncoding.DecodeString("FgMBARcBAAETAwPJL2jlt1OAo+Rslkjv/aqKiTthKMaCKg2Gvd+uALDbDCDdY+UIk8ouadEB9fC3j52Y1i7SJZqGIgBRIS6kKieYrAAoEwITAcAswCvAMMAvwCTAI8AowCfACsAJwBTAEwCdAJwAPQA8ADUALwEAAKIAAAAOAAwAAAlpcGluZm8uaW8ABQAFAQAAAAAAKwAJCAMEAwMDAgMBAA0AGgAYCAQIBQgGBAEFAQIBBAMFAwIDAgIGAQYDACMAAAAKAAgABgAdABcAGAAQAAsACQhodHRwLzEuMQAzACYAJAAdACBguQbqNJNyamYxYcrBFpBP7pWv5TgZsP9gwGtMYNKVBQAxAAAAFwAA/wEAAQAALQACAQE=") + assert.NoError(t, err) + index = 0 + reqAddr = "222.222.222.222:443" + putback, err = sniffer.TCP(stream, &reqAddr) + assert.NoError(t, err) + assert.Equal(t, *buf, putback) + assert.Equal(t, "ipinfo.io:443", reqAddr) + + // Test unrecognized 1 + *buf = []byte("Wait It's All Ohio? Always Has Been.") + index = 0 + reqAddr = "123.123.123.123:123" + putback, err = sniffer.TCP(stream, &reqAddr) + assert.NoError(t, err) + assert.Equal(t, *buf, putback) + assert.Equal(t, "123.123.123.123:123", reqAddr) + + // Test unrecognized 2 + *buf = []byte("\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a") + index = 0 + reqAddr = "45.45.45.45:45" + putback, err = sniffer.TCP(stream, &reqAddr) + assert.NoError(t, err) + assert.Equal(t, []byte("\x01\x02\x03"), putback) + assert.Equal(t, "45.45.45.45:45", reqAddr) + + // Test timeout + blockStream := &mockStream{} + blockStream.EXPECT().SetReadDeadline(mock.Anything).Return(nil) + blockStream.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) { + time.Sleep(2 * time.Second) + return 0, io.EOF + }) + reqAddr = "66.66.66.66:66" + putback, err = sniffer.TCP(blockStream, &reqAddr) + assert.NoError(t, err) + assert.Equal(t, []byte{}, putback) + assert.Equal(t, "66.66.66.66:66", reqAddr) +} + +func TestSnifferUDP(t *testing.T) { + sniffer := &Sniffer{ + Timeout: 1 * time.Second, + RewriteDomain: false, + } + + // Test QUIC + reqAddr := "2.3.4.5:443" + pkt, err := base64.StdEncoding.DecodeString("ygAAAAEIwugWgPS7ulYAAES8hY891uwgGE9GG4CPOLd+nsDe28raso24lCSFmlFwYQG1uF39ikbL13/R9ZTghYmTl+jEbr6F9TxxRiOgpTmKRmh6aKZiIiVfy5pVRckovaI8lq0WRoW9xoFNTyYtQP8TVJ3bLCK+zUqpquEQSyWf7CE43ywayyMpE9UlIoPXFWCoopXLM1SvzdQ+17P51N9KR7m4emti4DWWTBLMQOvrwd2HEEkbiZdRO1wf6ZXJlIat5dN0R/6uod60OFPO+u+awvq67MoMReC7+5I/xWI+xx6o4JpnZNn6YPG8Gqi8hS6doNcAAdtD8h5eMLuHCCgkpX3QVjjfWtcOhtw9xKjU43HhUPwzUTv+JDLgwuTQCTmlfYlb3B+pk4b2I9si0tJ0SBuYaZ2VQPtZbj2hpGXw3gn11pbN8xsbKkQL50+Scd4dGJxWQlGaJHeaU5WOCkxLXc635z8m5XO/CBHVYPGp4pfwfwNUgbe5WF+3MaUIlDB8dMfsnrO0BmZPo379jVx0SFLTAiS8wAdHib1WNEY8qKYnTWuiyxYg1GZEhJt0nXmI+8f0eJq42DgHBWC+Rf5rRBr/Sf25o3mFAmTUaul0Woo9/CIrpT73B63N91xd9A77i4ru995YG8l9Hen+eLtpDU9Q9376nwMDYBzeYG9U/Rn0Urbm6q4hmAgV/xlNJ2rAyDS+yLnwqD6I0PRy8bZJEttcidb/SkOyrpgMiAzWeT+SO+c/k+Y8H0UTRa05faZUrhuUaym9wAcaIVRA6nFI+fejfjVp+7afFv+kWn3vCqQEij+CRHuxkltrixZMD2rfYj6NUW7TTYBtPRtuV/V0ZIDjRR26vr4K+0D84+l3c0mA/l6nmpP5kkco3nmpdjtQN6sGXL7+5o0nnsftX5d6/n5mLyEpP+AEDl1zk3iqkS62RsITwql6DMMoGbSDdUpMclCIeM0vlo3CkxGMO7QA9ruVeNddkL3EWMivl+uxO43sXEEqYQHVl4N75y63t05GOf7/gm9Kb/BJ8MpG9ViEkVYaskQCzi3D8bVpzo8FfTj8te8B6c3ikc/cm7r8k0ZcZpr+YiLGDYq+0ilHxpqJfmq8dPkSvxdzLcUSvy7+LMQ/TTobRSF7L4JhtDKck0+00vl9H35Tkh9N+MsVtpKdWyoqZ4XaK2Nx1M6AieczXpdFc0y7lYPoUfF4IeW8WzeVUclol5ElYjkyFz/lDOGAe1bF2g5AYaGWCPiGleVZknNdD5ihB8W8Mfkt1pEwq2S97AHrppqkf/VoIfZzeqH8wUFw8fDDrZIpnoa0rW7HfwIQaqJhPCyB9Z6TVbV4x9UWmaHfVAcinCK/7o10dtaj3rvEqcUC/iPceGq3Tqv/p9GGNJ+Ci2JBjXqNxYr893Llk75VdPD9pM6y1SM0P80oXNy32VMtafkFFST8GpvvqWcxUJ93kzaY8RmU1g3XFOImSU2utU6+FUQ2Pn5uLwcfT2cTYfTpPGh+WXjSbZ6trqdEMEsLHybuPo2UN4WpVLXVQma3kSaHQggcLlEip8GhEUAy/xCb2eKqhI4HkDpDjwDnDVKufWlnRaOHf58cc8Woi+WT8JTOkHC+nBEG6fKRPHDG08U5yayIQIjI") + assert.NoError(t, err) + err = sniffer.UDP(pkt, &reqAddr) + assert.NoError(t, err) + assert.Equal(t, "www.notion.so:443", reqAddr) + + // Test unrecognized + pkt = []byte("oh my sweet summer child") + reqAddr = "90.90.90.90:90" + err = sniffer.UDP(pkt, &reqAddr) + assert.NoError(t, err) + assert.Equal(t, "90.90.90.90:90", reqAddr) +} diff --git a/v2rayng/hysteria/extras/trafficlogger/http.go b/v2rayng/hysteria/extras/trafficlogger/http.go new file mode 100644 index 0000000000..d8e6ebd4f1 --- /dev/null +++ b/v2rayng/hysteria/extras/trafficlogger/http.go @@ -0,0 +1,300 @@ +package trafficlogger + +import ( + "cmp" + "encoding/json" + "fmt" + "net/http" + "slices" + "strconv" + "strings" + "sync" + "time" + + "github.com/apernet/hysteria/core/v2/server" + "github.com/apernet/quic-go" +) + +const ( + indexHTML = ` Hysteria Traffic Stats API Server

This is a Hysteria Traffic Stats API server.

Check the documentation for usage.

` +) + +// TrafficStatsServer implements both server.TrafficLogger and http.Handler +// to provide a simple HTTP API to get the traffic stats per user. +type TrafficStatsServer interface { + server.TrafficLogger + http.Handler +} + +func NewTrafficStatsServer(secret string) TrafficStatsServer { + return &trafficStatsServerImpl{ + StatsMap: make(map[string]*trafficStatsEntry), + KickMap: make(map[string]struct{}), + OnlineMap: make(map[string]int), + StreamMap: make(map[quic.Stream]*server.StreamStats), + Secret: secret, + } +} + +type trafficStatsServerImpl struct { + Mutex sync.RWMutex + StatsMap map[string]*trafficStatsEntry + OnlineMap map[string]int + StreamMap map[quic.Stream]*server.StreamStats + KickMap map[string]struct{} + Secret string +} + +type trafficStatsEntry struct { + Tx uint64 `json:"tx"` + Rx uint64 `json:"rx"` +} + +func (s *trafficStatsServerImpl) LogTraffic(id string, tx, rx uint64) (ok bool) { + s.Mutex.Lock() + defer s.Mutex.Unlock() + + _, ok = s.KickMap[id] + if ok { + delete(s.KickMap, id) + return false + } + + entry, ok := s.StatsMap[id] + if !ok { + entry = &trafficStatsEntry{} + s.StatsMap[id] = entry + } + entry.Tx += tx + entry.Rx += rx + + return true +} + +// LogOnlineStateChanged updates the online state to the online map. +func (s *trafficStatsServerImpl) LogOnlineState(id string, online bool) { + s.Mutex.Lock() + defer s.Mutex.Unlock() + + if online { + s.OnlineMap[id]++ + } else { + s.OnlineMap[id]-- + if s.OnlineMap[id] <= 0 { + delete(s.OnlineMap, id) + } + } +} + +func (s *trafficStatsServerImpl) TraceStream(stream quic.Stream, stats *server.StreamStats) { + s.Mutex.Lock() + defer s.Mutex.Unlock() + + s.StreamMap[stream] = stats +} + +func (s *trafficStatsServerImpl) UntraceStream(stream quic.Stream) { + s.Mutex.Lock() + defer s.Mutex.Unlock() + + delete(s.StreamMap, stream) +} + +func (s *trafficStatsServerImpl) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if s.Secret != "" && r.Header.Get("Authorization") != s.Secret { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + if r.Method == http.MethodGet && r.URL.Path == "/" { + _, _ = w.Write([]byte(indexHTML)) + return + } + if r.Method == http.MethodGet && r.URL.Path == "/traffic" { + s.getTraffic(w, r) + return + } + if r.Method == http.MethodPost && r.URL.Path == "/kick" { + s.kick(w, r) + return + } + if r.Method == http.MethodGet && r.URL.Path == "/online" { + s.getOnline(w, r) + return + } + if r.Method == http.MethodGet && r.URL.Path == "/dump/streams" { + s.getDumpStreams(w, r) + return + } + http.NotFound(w, r) +} + +func (s *trafficStatsServerImpl) getTraffic(w http.ResponseWriter, r *http.Request) { + bClear, _ := strconv.ParseBool(r.URL.Query().Get("clear")) + var jb []byte + var err error + if bClear { + s.Mutex.Lock() + jb, err = json.Marshal(s.StatsMap) + s.StatsMap = make(map[string]*trafficStatsEntry) + s.Mutex.Unlock() + } else { + s.Mutex.RLock() + jb, err = json.Marshal(s.StatsMap) + s.Mutex.RUnlock() + } + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _, _ = w.Write(jb) +} + +func (s *trafficStatsServerImpl) getOnline(w http.ResponseWriter, r *http.Request) { + s.Mutex.RLock() + defer s.Mutex.RUnlock() + + jb, err := json.Marshal(s.OnlineMap) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _, _ = w.Write(jb) +} + +type dumpStreamEntry struct { + State string `json:"state"` + + Auth string `json:"auth"` + Connection uint32 `json:"connection"` + Stream uint64 `json:"stream"` + + ReqAddr string `json:"req_addr"` + HookedReqAddr string `json:"hooked_req_addr"` + + Tx uint64 `json:"tx"` + Rx uint64 `json:"rx"` + + InitialAt string `json:"initial_at"` + LastActiveAt string `json:"last_active_at"` + + // for text/plain output + initialTime time.Time + lastActiveTime time.Time +} + +func (e *dumpStreamEntry) fromStreamStats(stream quic.Stream, s *server.StreamStats) { + e.State = s.State.Load().String() + e.Auth = s.AuthID + e.Connection = s.ConnID + e.Stream = uint64(stream.StreamID()) + e.ReqAddr = s.ReqAddr.Load() + e.HookedReqAddr = s.HookedReqAddr.Load() + e.Tx = s.Tx.Load() + e.Rx = s.Rx.Load() + e.initialTime = s.InitialTime + e.lastActiveTime = s.LastActiveTime.Load() + e.InitialAt = e.initialTime.Format(time.RFC3339Nano) + e.LastActiveAt = e.lastActiveTime.Format(time.RFC3339Nano) +} + +func formatDumpStreamLine(state, auth, connection, stream, reqAddr, hookedReqAddr, tx, rx, lifetime, lastActive string) string { + return fmt.Sprintf("%-8s %-12s %12s %8s %12s %12s %12s %12s %-16s %s", state, auth, connection, stream, tx, rx, lifetime, lastActive, reqAddr, hookedReqAddr) +} + +func (e *dumpStreamEntry) String() string { + stateText := strings.ToUpper(e.State) + connectionText := fmt.Sprintf("%08X", e.Connection) + streamText := strconv.FormatUint(e.Stream, 10) + reqAddrText := e.ReqAddr + if reqAddrText == "" { + reqAddrText = "-" + } + hookedReqAddrText := e.HookedReqAddr + if hookedReqAddrText == "" { + hookedReqAddrText = "-" + } + txText := strconv.FormatUint(e.Tx, 10) + rxText := strconv.FormatUint(e.Rx, 10) + lifetime := time.Now().Sub(e.initialTime) + if lifetime < 10*time.Minute { + lifetime = lifetime.Round(time.Millisecond) + } else { + lifetime = lifetime.Round(time.Second) + } + lastActive := time.Now().Sub(e.lastActiveTime) + if lastActive < 10*time.Minute { + lastActive = lastActive.Round(time.Millisecond) + } else { + lastActive = lastActive.Round(time.Second) + } + + return formatDumpStreamLine(stateText, e.Auth, connectionText, streamText, reqAddrText, hookedReqAddrText, txText, rxText, lifetime.String(), lastActive.String()) +} + +func (s *trafficStatsServerImpl) getDumpStreams(w http.ResponseWriter, r *http.Request) { + var entries []dumpStreamEntry + + s.Mutex.RLock() + entries = make([]dumpStreamEntry, len(s.StreamMap)) + index := 0 + for stream, stats := range s.StreamMap { + entries[index].fromStreamStats(stream, stats) + index++ + } + s.Mutex.RUnlock() + + slices.SortFunc(entries, func(lhs, rhs dumpStreamEntry) int { + if ret := cmp.Compare(lhs.Auth, rhs.Auth); ret != 0 { + return ret + } + if ret := cmp.Compare(lhs.Connection, rhs.Connection); ret != 0 { + return ret + } + if ret := cmp.Compare(lhs.Stream, rhs.Stream); ret != 0 { + return ret + } + return 0 + }) + + accept := r.Header.Get("Accept") + + if strings.Contains(accept, "text/plain") { + // Generate netstat-like output for humans + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + + // Print table header + _, _ = fmt.Fprintln(w, formatDumpStreamLine("State", "Auth", "Connection", "Stream", "Req-Addr", "Hooked-Req-Addr", "TX-Bytes", "RX-Bytes", "Lifetime", "Last-Active")) + for _, entry := range entries { + _, _ = fmt.Fprintln(w, entry.String()) + } + return + } + + // Response with json by default + wrapper := struct { + Streams []dumpStreamEntry `json:"streams"` + }{entries} + w.Header().Set("Content-Type", "application/json; charset=utf-8") + err := json.NewEncoder(w).Encode(&wrapper) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (s *trafficStatsServerImpl) kick(w http.ResponseWriter, r *http.Request) { + var ids []string + err := json.NewDecoder(r.Body).Decode(&ids) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + s.Mutex.Lock() + for _, id := range ids { + s.KickMap[id] = struct{}{} + } + s.Mutex.Unlock() + + w.WriteHeader(http.StatusOK) +} diff --git a/v2rayng/hysteria/extras/transport/udphop/addr.go b/v2rayng/hysteria/extras/transport/udphop/addr.go new file mode 100644 index 0000000000..afde26a3b3 --- /dev/null +++ b/v2rayng/hysteria/extras/transport/udphop/addr.go @@ -0,0 +1,67 @@ +package udphop + +import ( + "fmt" + "net" + + "github.com/apernet/hysteria/extras/v2/utils" +) + +type InvalidPortError struct { + PortStr string +} + +func (e InvalidPortError) Error() string { + return fmt.Sprintf("%s is not a valid port number or range", e.PortStr) +} + +// UDPHopAddr contains an IP address and a list of ports. +type UDPHopAddr struct { + IP net.IP + Ports []uint16 + PortStr string +} + +func (a *UDPHopAddr) Network() string { + return "udphop" +} + +func (a *UDPHopAddr) String() string { + return net.JoinHostPort(a.IP.String(), a.PortStr) +} + +// addrs returns a list of net.Addr's, one for each port. +func (a *UDPHopAddr) addrs() ([]net.Addr, error) { + var addrs []net.Addr + for _, port := range a.Ports { + addr := &net.UDPAddr{ + IP: a.IP, + Port: int(port), + } + addrs = append(addrs, addr) + } + return addrs, nil +} + +func ResolveUDPHopAddr(addr string) (*UDPHopAddr, error) { + host, portStr, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + ip, err := net.ResolveIPAddr("ip", host) + if err != nil { + return nil, err + } + result := &UDPHopAddr{ + IP: ip.IP, + PortStr: portStr, + } + + pu := utils.ParsePortUnion(portStr) + if pu == nil { + return nil, InvalidPortError{portStr} + } + result.Ports = pu.Ports() + + return result, nil +} diff --git a/v2rayng/hysteria/extras/transport/udphop/conn.go b/v2rayng/hysteria/extras/transport/udphop/conn.go new file mode 100644 index 0000000000..32cc31c068 --- /dev/null +++ b/v2rayng/hysteria/extras/transport/udphop/conn.go @@ -0,0 +1,294 @@ +package udphop + +import ( + "errors" + "math/rand" + "net" + "sync" + "syscall" + "time" +) + +const ( + packetQueueSize = 1024 + udpBufferSize = 2048 // QUIC packets are at most 1500 bytes long, so 2k should be more than enough + + defaultHopInterval = 30 * time.Second +) + +type udpHopPacketConn struct { + Addr net.Addr + Addrs []net.Addr + HopInterval time.Duration + ListenUDPFunc ListenUDPFunc + + connMutex sync.RWMutex + prevConn net.PacketConn + currentConn net.PacketConn + addrIndex int + + readBufferSize int + writeBufferSize int + + recvQueue chan *udpPacket + closeChan chan struct{} + closed bool + + bufPool sync.Pool +} + +type udpPacket struct { + Buf []byte + N int + Addr net.Addr + Err error +} + +type ListenUDPFunc = func() (net.PacketConn, error) + +func NewUDPHopPacketConn(addr *UDPHopAddr, hopInterval time.Duration, listenUDPFunc ListenUDPFunc) (net.PacketConn, error) { + if hopInterval == 0 { + hopInterval = defaultHopInterval + } else if hopInterval < 5*time.Second { + return nil, errors.New("hop interval must be at least 5 seconds") + } + if listenUDPFunc == nil { + listenUDPFunc = func() (net.PacketConn, error) { + return net.ListenUDP("udp", nil) + } + } + addrs, err := addr.addrs() + if err != nil { + return nil, err + } + curConn, err := listenUDPFunc() + if err != nil { + return nil, err + } + hConn := &udpHopPacketConn{ + Addr: addr, + Addrs: addrs, + HopInterval: hopInterval, + ListenUDPFunc: listenUDPFunc, + prevConn: nil, + currentConn: curConn, + addrIndex: rand.Intn(len(addrs)), + recvQueue: make(chan *udpPacket, packetQueueSize), + closeChan: make(chan struct{}), + bufPool: sync.Pool{ + New: func() interface{} { + return make([]byte, udpBufferSize) + }, + }, + } + go hConn.recvLoop(curConn) + go hConn.hopLoop() + return hConn, nil +} + +func (u *udpHopPacketConn) recvLoop(conn net.PacketConn) { + for { + buf := u.bufPool.Get().([]byte) + n, addr, err := conn.ReadFrom(buf) + if err != nil { + u.bufPool.Put(buf) + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + // Only pass through timeout errors here, not permanent errors + // like connection closed. Connection close is normal as we close + // the old connection to exit this loop every time we hop. + u.recvQueue <- &udpPacket{nil, 0, nil, netErr} + } + return + } + select { + case u.recvQueue <- &udpPacket{buf, n, addr, nil}: + // Packet successfully queued + default: + // Queue is full, drop the packet + u.bufPool.Put(buf) + } + } +} + +func (u *udpHopPacketConn) hopLoop() { + ticker := time.NewTicker(u.HopInterval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + u.hop() + case <-u.closeChan: + return + } + } +} + +func (u *udpHopPacketConn) hop() { + u.connMutex.Lock() + defer u.connMutex.Unlock() + if u.closed { + return + } + newConn, err := u.ListenUDPFunc() + if err != nil { + // Could be temporary, just skip this hop + return + } + // We need to keep receiving packets from the previous connection, + // because otherwise there will be packet loss due to the time gap + // between we hop to a new port and the server acknowledges this change. + // So we do the following: + // Close prevConn, + // move currentConn to prevConn, + // set newConn as currentConn, + // start recvLoop on newConn. + if u.prevConn != nil { + _ = u.prevConn.Close() // recvLoop for this conn will exit + } + u.prevConn = u.currentConn + u.currentConn = newConn + // Set buffer sizes if previously set + if u.readBufferSize > 0 { + _ = trySetReadBuffer(u.currentConn, u.readBufferSize) + } + if u.writeBufferSize > 0 { + _ = trySetWriteBuffer(u.currentConn, u.writeBufferSize) + } + go u.recvLoop(newConn) + // Update addrIndex to a new random value + u.addrIndex = rand.Intn(len(u.Addrs)) +} + +func (u *udpHopPacketConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { + for { + select { + case p := <-u.recvQueue: + if p.Err != nil { + return 0, nil, p.Err + } + // Currently we do not check whether the packet is from + // the server or not due to performance reasons. + n := copy(b, p.Buf[:p.N]) + u.bufPool.Put(p.Buf) + return n, u.Addr, nil + case <-u.closeChan: + return 0, nil, net.ErrClosed + } + } +} + +func (u *udpHopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { + u.connMutex.RLock() + defer u.connMutex.RUnlock() + if u.closed { + return 0, net.ErrClosed + } + // Skip the check for now, always write to the server, + // for the same reason as in ReadFrom. + return u.currentConn.WriteTo(b, u.Addrs[u.addrIndex]) +} + +func (u *udpHopPacketConn) Close() error { + u.connMutex.Lock() + defer u.connMutex.Unlock() + if u.closed { + return nil + } + // Close prevConn and currentConn + // Close closeChan to unblock ReadFrom & hopLoop + // Set closed flag to true to prevent double close + if u.prevConn != nil { + _ = u.prevConn.Close() + } + err := u.currentConn.Close() + close(u.closeChan) + u.closed = true + u.Addrs = nil // For GC + return err +} + +func (u *udpHopPacketConn) LocalAddr() net.Addr { + u.connMutex.RLock() + defer u.connMutex.RUnlock() + return u.currentConn.LocalAddr() +} + +func (u *udpHopPacketConn) SetDeadline(t time.Time) error { + u.connMutex.RLock() + defer u.connMutex.RUnlock() + if u.prevConn != nil { + _ = u.prevConn.SetDeadline(t) + } + return u.currentConn.SetDeadline(t) +} + +func (u *udpHopPacketConn) SetReadDeadline(t time.Time) error { + u.connMutex.RLock() + defer u.connMutex.RUnlock() + if u.prevConn != nil { + _ = u.prevConn.SetReadDeadline(t) + } + return u.currentConn.SetReadDeadline(t) +} + +func (u *udpHopPacketConn) SetWriteDeadline(t time.Time) error { + u.connMutex.RLock() + defer u.connMutex.RUnlock() + if u.prevConn != nil { + _ = u.prevConn.SetWriteDeadline(t) + } + return u.currentConn.SetWriteDeadline(t) +} + +// UDP-specific methods below + +func (u *udpHopPacketConn) SetReadBuffer(bytes int) error { + u.connMutex.Lock() + defer u.connMutex.Unlock() + u.readBufferSize = bytes + if u.prevConn != nil { + _ = trySetReadBuffer(u.prevConn, bytes) + } + return trySetReadBuffer(u.currentConn, bytes) +} + +func (u *udpHopPacketConn) SetWriteBuffer(bytes int) error { + u.connMutex.Lock() + defer u.connMutex.Unlock() + u.writeBufferSize = bytes + if u.prevConn != nil { + _ = trySetWriteBuffer(u.prevConn, bytes) + } + return trySetWriteBuffer(u.currentConn, bytes) +} + +func (u *udpHopPacketConn) SyscallConn() (syscall.RawConn, error) { + u.connMutex.RLock() + defer u.connMutex.RUnlock() + sc, ok := u.currentConn.(syscall.Conn) + if !ok { + return nil, errors.New("not supported") + } + return sc.SyscallConn() +} + +func trySetReadBuffer(pc net.PacketConn, bytes int) error { + sc, ok := pc.(interface { + SetReadBuffer(bytes int) error + }) + if ok { + return sc.SetReadBuffer(bytes) + } + return nil +} + +func trySetWriteBuffer(pc net.PacketConn, bytes int) error { + sc, ok := pc.(interface { + SetWriteBuffer(bytes int) error + }) + if ok { + return sc.SetWriteBuffer(bytes) + } + return nil +} diff --git a/v2rayng/hysteria/extras/utils/portunion.go b/v2rayng/hysteria/extras/utils/portunion.go new file mode 100644 index 0000000000..f76a6fd0a6 --- /dev/null +++ b/v2rayng/hysteria/extras/utils/portunion.go @@ -0,0 +1,107 @@ +package utils + +import ( + "sort" + "strconv" + "strings" +) + +// PortUnion is a collection of multiple port ranges. +type PortUnion []PortRange + +// PortRange represents a range of ports. +// Start and End are inclusive. [Start, End] +type PortRange struct { + Start, End uint16 +} + +// ParsePortUnion parses a string of comma-separated port ranges (or single ports) into a PortUnion. +// Returns nil if the input is invalid. +// The returned PortUnion is guaranteed to be normalized. +func ParsePortUnion(s string) PortUnion { + if s == "all" || s == "*" { + // Wildcard special case + return PortUnion{PortRange{0, 65535}} + } + var result PortUnion + portStrs := strings.Split(s, ",") + for _, portStr := range portStrs { + if strings.Contains(portStr, "-") { + // Port range + portRange := strings.Split(portStr, "-") + if len(portRange) != 2 { + return nil + } + start, err := strconv.ParseUint(portRange[0], 10, 16) + if err != nil { + return nil + } + end, err := strconv.ParseUint(portRange[1], 10, 16) + if err != nil { + return nil + } + if start > end { + start, end = end, start + } + result = append(result, PortRange{uint16(start), uint16(end)}) + } else { + // Single port + port, err := strconv.ParseUint(portStr, 10, 16) + if err != nil { + return nil + } + result = append(result, PortRange{uint16(port), uint16(port)}) + } + } + if result == nil { + return nil + } + return result.Normalize() +} + +// Normalize normalizes a PortUnion. +// No overlapping ranges, ranges are sorted from low to high. +func (u PortUnion) Normalize() PortUnion { + if len(u) == 0 { + return u + } + sort.Slice(u, func(i, j int) bool { + if u[i].Start == u[j].Start { + return u[i].End < u[j].End + } + return u[i].Start < u[j].Start + }) + normalized := PortUnion{u[0]} + for _, current := range u[1:] { + last := &normalized[len(normalized)-1] + if uint32(current.Start) <= uint32(last.End)+1 { + if current.End > last.End { + last.End = current.End + } + } else { + normalized = append(normalized, current) + } + } + return normalized +} + +// Ports returns all ports in the PortUnion as a slice. +func (u PortUnion) Ports() []uint16 { + var ports []uint16 + for _, r := range u { + for i := uint32(r.Start); i <= uint32(r.End); i++ { + ports = append(ports, uint16(i)) + } + } + return ports +} + +// Contains returns true if the PortUnion contains the given port. +func (u PortUnion) Contains(port uint16) bool { + for _, r := range u { + if port >= r.Start && port <= r.End { + return true + } + } + return false +} diff --git a/v2rayng/hysteria/extras/utils/portunion_test.go b/v2rayng/hysteria/extras/utils/portunion_test.go new file mode 100644 index 0000000000..ba056a3741 --- /dev/null +++ b/v2rayng/hysteria/extras/utils/portunion_test.go @@ -0,0 +1,150 @@ +package utils + +import ( + "reflect" + "slices" + "testing" +) + +func TestParsePortUnion(t *testing.T) { + tests := []struct { + name string + s string + want PortUnion + }{ + { + name: "empty", + s: "", + want: nil, + }, + { + name: "all 1", + s: "all", + want: PortUnion{{0, 65535}}, + }, + { + name: "all 2", + s: "*", + want: PortUnion{{0, 65535}}, + }, + { + name: "single port", + s: "1234", + want: PortUnion{{1234, 1234}}, + }, + { + name: "multiple ports (unsorted)", + s: "5678,1234,9012", + want: PortUnion{{1234, 1234}, {5678, 5678}, {9012, 9012}}, + }, + { + name: "one range", + s: "1234-1240", + want: PortUnion{{1234, 1240}}, + }, + { + name: "one range (reversed)", + s: "1240-1234", + want: PortUnion{{1234, 1240}}, + }, + { + name: "multiple ports and ranges (reversed, unsorted, overlapping)", + s: "5678,1200-1236,9100-9012,1234-1240", + want: PortUnion{{1200, 1240}, {5678, 5678}, {9012, 9100}}, + }, + { + name: "multiple ports and ranges with 65535 (reversed, unsorted, overlapping)", + s: "5678,1200-1236,65531-65535,65532-65534,9100-9012,1234-1240", + want: PortUnion{{1200, 1240}, {5678, 5678}, {9012, 9100}, {65531, 65535}}, + }, + { + name: "multiple ports and ranges with 65535 (reversed, unsorted, overlapping) 2", + s: "5678,1200-1236,65532-65535,65531-65534,9100-9012,1234-1240", + want: PortUnion{{1200, 1240}, {5678, 5678}, {9012, 9100}, {65531, 65535}}, + }, + { + name: "invalid 1", + s: "1234-", + want: nil, + }, + { + name: "invalid 2", + s: "1234-ggez", + want: nil, + }, + { + name: "invalid 3", + s: "233,", + want: nil, + }, + { + name: "invalid 4", + s: "1234-1240-1250", + want: nil, + }, + { + name: "invalid 5", + s: "-,,", + want: nil, + }, + { + name: "invalid 6", + s: "http", + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ParsePortUnion(tt.s); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParsePortUnion() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPortUnion_Ports(t *testing.T) { + tests := []struct { + name string + pu PortUnion + want []uint16 + }{ + { + name: "single port", + pu: PortUnion{{1234, 1234}}, + want: []uint16{1234}, + }, + { + name: "multiple ports", + pu: PortUnion{{1234, 1236}}, + want: []uint16{1234, 1235, 1236}, + }, + { + name: "multiple ports and ranges", + pu: PortUnion{{1234, 1236}, {5678, 5680}, {9000, 9002}}, + want: []uint16{1234, 1235, 1236, 5678, 5679, 5680, 9000, 9001, 9002}, + }, + { + name: "single port 65535", + pu: PortUnion{{65535, 65535}}, + want: []uint16{65535}, + }, + { + name: "port range with 65535", + pu: PortUnion{{65530, 65535}}, + want: []uint16{65530, 65531, 65532, 65533, 65534, 65535}, + }, + { + name: "multiple ports and ranges with 65535", + pu: PortUnion{{65530, 65535}, {1234, 1236}}, + want: []uint16{65530, 65531, 65532, 65533, 65534, 65535, 1234, 1235, 1236}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.pu.Ports(); !slices.Equal(got, tt.want) { + t.Errorf("PortUnion.Ports() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/v2rayng/hysteria/go.work.sum b/v2rayng/hysteria/go.work.sum new file mode 100644 index 0000000000..79da3faf24 --- /dev/null +++ b/v2rayng/hysteria/go.work.sum @@ -0,0 +1,390 @@ +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y= +cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= +cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA= +cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0= +cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= +cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA= +cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= +cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= +cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= +cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU= +cloud.google.com/go/storage v1.14.0 h1:6RRlFMv1omScs6iq2hfE3IvgE+l6RfJPampq8UZc5TU= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3 h1:hJiie5Bf3QucGRa4ymsAUOxyhYwGEz1xrsVk0P8erlw= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0 h1:SPOUaucgtVls75mg+X7CXigS71EnsfVUK/2CgVrwqgw= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412 h1:GvWw74lx5noHocd+f6HBMXK6DuggBB1dhVkuGZbv7qM= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c h1:ivON6cwHK1OH26MZyWDCnbTRZZf0IhNsENoNAKFS1g4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999 h1:OR8VhtwhcAI3U48/rzBsVOuHi0zDPzYI1xASVcdSgR8= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/armon/go-metrics v0.4.0 h1:yCQqn7dwca4ITXb+CbubHmedzaQYHhNhrEXLYUeEe8Q= +github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625 h1:ckJgFhFWywOx+YLEMIJsTb+NV6NexWICk5+AMSuz3ss= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 h1:D21IyuvjDCshj1/qq+pCNd3VZOAEI9jy6Bi131YlXgI= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d h1:t5Wuyh53qYyg9eqn4BbnlIT+vmhyww0TatL+zT3uWgI= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415 h1:q1oJaUPdmpDm/VyXosjgPgr6wS7c5iV2p0PwJD73bUI= +github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad h1:EmNYJhPYy0pOFjCx2PrgtaBXmee0iUX9hLlxE1xHOJE= +github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1 h1:j3L6gSLQalDETeEg/Jg0mGY0/y/N6zI2xX1978P0Uqw= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 h1:2hRPrmiwPrp3fQX967rNJIhQPtiGXdlQWAxKbKw3VHA= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian/v3 v3.1.0 h1:wCKgOCHuUEVfsaQLpPSJb7VdYCdTVZQAuOdYm1yc/60= +github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/googleapis/enterprise-certificate-proxy v0.2.1 h1:RY7tHKZcRlk788d5WSo/e83gOyyy742E8GSs771ySpg= +github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= +github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0 h1:WcmKMm43DR7RdtlkEXQJyo5ws8iTp98CyhCCbOHMvNI= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/consul/api v1.18.0 h1:R7PPNzTCeN6VuQNDwwhZWJvzCtGSrNpJqfb22h3yH9g= +github.com/hashicorp/consul/api v1.18.0/go.mod h1:owRRGJ9M5xReDC5nfT8FTJrNAPbT4NM6p/k+d03q2v4= +github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= +github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1 h1:ujPKutqRlJtcfWk6toYVYagwra7HQHbXOaS171b4Tg8= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= +github.com/kr/pty v1.1.3 h1:/Um6a/ZmD5tF7peoOJ5oN5KMQ0DrGVQSXLNwyckutPk= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe h1:W/GaMY0y69G4cFlmsC6B9sbuo2fP8OFP1ABjt4kPz+w= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 h1:8Q0qkMVC/MmWkpIdlvZgcv2o2jrlF6zqVOh7W5YHdMA= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= +github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 h1:D6paGObi5Wud7xg83MaEFyjxQB1W5bz5d0IFppr+ymk= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab h1:eFXv9Nu1lGbrNbj619aWwZfVF5HBrm9Plte8aNptuTI= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/openzipkin/zipkin-go v0.1.1 h1:A/ADD6HaPnAKj3yS7HjGHRK77qi41Hi0DirOOIQAeIw= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= +github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/sagikazarmark/crypt v0.9.0 h1:fipzMFW34hFUEc4D7fsLQFtE7yElkpgyS2zruedRdZk= +github.com/sagikazarmark/crypt v0.9.0/go.mod h1:RnH7sEhxfdnPm1z+XMgSLjWTEIjyK4z2dw6+4vHTMuo= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4 h1:Fth6mevc5rX7glNLpbAMJnqKlfIkcTjZCSHEeqvKbcI= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48 h1:vabduItPAIz9px5iryD5peyx7O3Ya8TBThapgXim98o= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470 h1:qb9IthCFBmROJ6YBS31BEMeSYjOscSiG+EO+JVNTz64= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d h1:Yoy/IzG4lULT6qZg62sVC+qyBL8DQkmD2zv6i7OImrc= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c h1:UOk+nlt1BJtTcH15CT7iNO7YVWTfTv/DNwEAQHLIaDQ= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b h1:vYEG87HxbU6dXj5npkeulCS96Dtz5xg3jcfCgpcvbIw= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20 h1:7pDq9pAMCQgRohFmd25X8hIH8VxmT3TaDm+r9LHxgBk= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9 h1:MPblCbqA5+z6XARjScMfz1TqtJC7TuTRj0U9VqIBs6k= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50 h1:crYRwvwjdVh1biHzzciFHe8DrZcYrVcZFlJtykhRctg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc h1:eHRtZoIi6n9Wo1uR+RU44C247msLWwyA89hVKwRLkMk= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 h1:SWV2fHctRpRrp49VXJ6UZja7gU9QLHwRpIPBN89SKEo= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9 h1:fxoFD0in0/CBzXoyNhMTjvBZYW6ilSnTw7N7y/8vkmM= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191 h1:T4wuULTrzCKMFlg3HmKHgXAF8oStFb/+lOIupLV2v+o= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241 h1:Y+TeIabU8sJD10Qwd/zMty2/LEaT9GNDaA6nyZf+jgo= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122 h1:TQVQrsyNaimGwF7bIhzoVC9QkKm4KsWd8cECGzFx8gI= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2 h1:bu666BQci+y4S0tVRVjsHUeRon6vUXmsGBwdowgMrg4= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82 h1:LneqU9PHDsg/AkPDU3AkqMxnMYL+imaqkpflHu73us8= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537 h1:YGaxtkYjb8mnTvtufv2LKLwCQu2/C7qFB7UtrOlTWOY= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133 h1:JtcyT0rk/9PKOdnKQzuDR+FSjh7SGtJwpgVpfZBRKlQ= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/viant/assertly v0.4.8 h1:5x1GzBaRteIwTr5RAGFVG14uNeRFxVNbXPWrK2qAgpc= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0 h1:6TteTDQ68CjgcCe8wH3D3ZhUQQOJXMTbj/D9rkk2a1k= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +go.etcd.io/etcd/api/v3 v3.5.6 h1:Cy2qx3npLcYqTKqGJzMypnMv2tiRyifZJ17BlWIWA7A= +go.etcd.io/etcd/api/v3 v3.5.6/go.mod h1:KFtNaxGDw4Yx/BA4iPPwevUTAuqcsPxzyX8PHydchN8= +go.etcd.io/etcd/client/pkg/v3 v3.5.6 h1:TXQWYceBKqLp4sa87rcPs11SXxUA/mHwH975v+BDvLU= +go.etcd.io/etcd/client/pkg/v3 v3.5.6/go.mod h1:ggrwbk069qxpKPq8/FKkQ3Xq9y39kbFR4LnKszpRXeQ= +go.etcd.io/etcd/client/v2 v2.305.6 h1:fIDR0p4KMjw01MJMfUIDWdQbjo06PD6CeYM5z4EHLi0= +go.etcd.io/etcd/client/v2 v2.305.6/go.mod h1:BHha8XJGe8vCIBfWBpbBLVZ4QjOIlfoouvOwydu63E0= +go.etcd.io/etcd/client/v3 v3.5.6 h1:coLs69PWCXE9G4FKquzNaSHrRyMCAXwF+IX1tAPVO8E= +go.etcd.io/etcd/client/v3 v3.5.6/go.mod h1:f6GRinRMCsFVv9Ht42EyY7nfsVGwrNO0WEoS2pRKzQk= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go4.org v0.0.0-20180809161055-417644f6feb5 h1:+hE86LblG4AyDgwMCLTE6FOlM9+qjHSYS+rKqxUVdsM= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d h1:E2M5QgjZ/Jg+ObCQAudsXxuTsLj7Nl5RV/lZcQZmKSo= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a h1:Jw5wfR+h9mnIYH+OtGT2im5wV1YGGDora5vTv/aa5bE= +golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852 h1:xYq6+9AtI+xP3M4r0N1hCkHrInHDBohhquRgx9Kk6gI= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= +golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/api v0.107.0 h1:I2SlFjD8ZWabaIFOfeEDg3pf0BHJDh6iYQ1ic3Yu/UU= +google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef h1:uQ2vjV/sHTsWSqdKeLqmwitzgvjMl7o4IdtHwUDXSJY= +google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.52.0 h1:kd48UiU7EHsV4rnLyOJRuP/Il/UHE7gdDAQ+SZI7nZk= +google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 h1:tmXTu+dfa+d9Evp8NpJdgOy6+rt8/x4yG7qPBrtNfLY= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= +honnef.co/go/tools v0.4.5 h1:YGD4H+SuIOOqsyoLOpZDWcieM28W47/zRO7f+9V3nvo= +honnef.co/go/tools v0.4.5/go.mod h1:GUV+uIBCLpdf0/v6UhHHG/yzI/z6qPskBeQCjcNB96k= +rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= +rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= +rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= +sourcegraph.com/sourcegraph/go-diff v0.5.0 h1:eTiIR0CoWjGzJcnQ3OkhIl/b9GJovq4lSAVRt0ZFEG8= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4 h1:JPJh2pk3+X4lXAkZIk2RuE/7/FoK9maXw+TNPJhVS/c= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/v2rayng/hysteria/hyperbole.py b/v2rayng/hysteria/hyperbole.py new file mode 100755 index 0000000000..ecc248dbf3 --- /dev/null +++ b/v2rayng/hysteria/hyperbole.py @@ -0,0 +1,550 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import argparse +import os +import re +import sys +import subprocess +import datetime +import shutil + +# Hyperbole is the official build script for Hysteria. +# Available environment variables for controlling the build: +# - HY_APP_VERSION: App version +# - HY_APP_COMMIT: App commit hash +# - HY_APP_PLATFORMS: Platforms to build for (e.g. "windows/amd64,linux/arm") + + +LOGO = """ +░█░█░█░█░█▀█░█▀▀░█▀▄░█▀▄░█▀█░█░░░█▀▀ +░█▀█░░█░░█▀▀░█▀▀░█▀▄░█▀▄░█░█░█░░░█▀▀ +░▀░▀░░▀░░▀░░░▀▀▀░▀░▀░▀▀░░▀▀▀░▀▀▀░▀▀▀ +""" + +DESC = "Hyperbole is the official build script for Hysteria." + +BUILD_DIR = "build" + +CORE_SRC_DIR = "./core" +EXTRAS_SRC_DIR = "./extras" +APP_SRC_DIR = "./app" +APP_SRC_CMD_PKG = "github.com/apernet/hysteria/app/v2/cmd" + +MODULE_SRC_DIRS = [CORE_SRC_DIR, EXTRAS_SRC_DIR, APP_SRC_DIR] + +ARCH_ALIASES = { + "arm": { + "GOARCH": "arm", + "GOARM": "7", + }, + "armv5": { + "GOARCH": "arm", + "GOARM": "5", + }, + "armv6": { + "GOARCH": "arm", + "GOARM": "6", + }, + "armv7": { + "GOARCH": "arm", + "GOARM": "7", + }, + "mips": { + "GOARCH": "mips", + "GOMIPS": "", + }, + "mipsle": { + "GOARCH": "mipsle", + "GOMIPS": "", + }, + "mips-sf": { + "GOARCH": "mips", + "GOMIPS": "softfloat", + }, + "mipsle-sf": { + "GOARCH": "mipsle", + "GOMIPS": "softfloat", + }, + "amd64": { + "GOARCH": "amd64", + "GOAMD64": "", + }, + "amd64-avx": { + "GOARCH": "amd64", + "GOAMD64": "v3", + }, +} + + +def check_command(args): + try: + subprocess.check_call( + args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + return True + except Exception: + return False + + +def check_build_env(): + if not check_command(["git", "--version"]): + print("Git is not installed. Please install Git and try again.") + return False + if not check_command(["git", "rev-parse", "--is-inside-work-tree"]): + print("Not in a Git repository. Please go to the project root and try again.") + return False + if not check_command(["go", "version"]): + print("Go is not installed. Please install Go and try again.") + return False + return True + + +def get_app_version(): + app_version = os.environ.get("HY_APP_VERSION") + if not app_version: + try: + output = ( + subprocess.check_output( + ["git", "describe", "--tags", "--always", "--match", "app/v*"] + ) + .decode() + .strip() + ) + app_version = output.split("/")[-1] + except Exception: + app_version = "Unknown" + return app_version + + +def get_app_version_code(str=None): + if not str: + str = get_app_version() + + match = re.search(r"v(\d+)\.(\d+)\.(\d+)", str) + + if match: + major, minor, patch = match.groups() + major = major.zfill(2)[:2] + minor = minor.zfill(2)[:2] + patch = patch.zfill(2)[:2] + return int(f"{major}{minor}{patch[:2]}") + else: + return 0 + + +def get_app_commit(): + app_commit = os.environ.get("HY_APP_COMMIT") + if not app_commit: + try: + app_commit = ( + subprocess.check_output(["git", "rev-parse", "HEAD"]).decode().strip() + ) + except Exception: + app_commit = "Unknown" + return app_commit + + +def get_toolchain(): + try: + output = subprocess.check_output(["go", "version"]).decode().strip() + if output.startswith("go version "): + output = output[11:] + return output + except Exception: + return "Unknown" + + +def get_current_os_arch(): + d_os = subprocess.check_output(["go", "env", "GOOS"]).decode().strip() + d_arch = subprocess.check_output(["go", "env", "GOARCH"]).decode().strip() + return (d_os, d_arch) + + +def get_lib_version(): + try: + with open(CORE_SRC_DIR + "/go.mod") as f: + for line in f: + line = line.strip() + if line.startswith("github.com/apernet/quic-go"): + return line.split(" ")[1].strip() + except Exception: + return "Unknown" + + +def get_app_platforms(): + platforms = os.environ.get("HY_APP_PLATFORMS") + if not platforms: + d_os, d_arch = get_current_os_arch() + return [(d_os, d_arch)] + + result = [] + for platform in platforms.split(","): + platform = platform.strip() + if not platform: + continue + parts = platform.split("/") + if len(parts) != 2: + continue + result.append((parts[0], parts[1])) + return result + + +def cmd_build(pprof=False, release=False, race=False): + if not check_build_env(): + return + + os.makedirs(BUILD_DIR, exist_ok=True) + + app_version = get_app_version() + app_date = datetime.datetime.now(datetime.timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) + app_toolchain = get_toolchain() + app_commit = get_app_commit() + lib_version = get_lib_version() + + ldflags = [ + "-X", + APP_SRC_CMD_PKG + ".appVersion=" + app_version, + "-X", + APP_SRC_CMD_PKG + ".appDate=" + app_date, + "-X", + APP_SRC_CMD_PKG + + ".appType=" + + ("release" if release else "dev") + + ("-pprof" if pprof else ""), + "-X", + '"' + APP_SRC_CMD_PKG + ".appToolchain=" + app_toolchain + '"', + "-X", + APP_SRC_CMD_PKG + ".appCommit=" + app_commit, + "-X", + APP_SRC_CMD_PKG + ".libVersion=" + lib_version, + ] + if release: + ldflags.append("-s") + ldflags.append("-w") + + for os_name, arch in get_app_platforms(): + print("Building for %s/%s..." % (os_name, arch)) + + out_name = "hysteria-%s-%s" % (os_name, arch) + if os_name == "windows": + out_name += ".exe" + + env = os.environ.copy() + env["GOOS"] = os_name + if arch in ARCH_ALIASES: + for k, v in ARCH_ALIASES[arch].items(): + env[k] = v + else: + env["GOARCH"] = arch + if os_name == "android": + env["CGO_ENABLED"] = "1" + ANDROID_NDK_HOME = ( + os.environ.get("ANDROID_NDK_HOME") + + "/toolchains/llvm/prebuilt/linux-x86_64/bin" + ) + if arch == "arm64": + env["CC"] = ANDROID_NDK_HOME + "/aarch64-linux-android29-clang" + elif arch == "armv7": + env["CC"] = ANDROID_NDK_HOME + "/armv7a-linux-androideabi29-clang" + elif arch == "386": + env["CC"] = ANDROID_NDK_HOME + "/i686-linux-android29-clang" + elif arch == "amd64": + env["CC"] = ANDROID_NDK_HOME + "/x86_64-linux-android29-clang" + else: + print("Unsupported arch for android: %s" % arch) + return + else: + env["CGO_ENABLED"] = "1" if race else "0" # Race detector requires cgo + + plat_ldflags = ldflags.copy() + plat_ldflags.append("-X") + plat_ldflags.append(APP_SRC_CMD_PKG + ".appPlatform=" + os_name) + plat_ldflags.append("-X") + plat_ldflags.append(APP_SRC_CMD_PKG + ".appArch=" + arch) + + cmd = [ + "go", + "build", + "-o", + os.path.join(BUILD_DIR, out_name), + "-ldflags", + " ".join(plat_ldflags), + ] + if pprof: + cmd.append("-tags") + cmd.append("pprof") + if race: + cmd.append("-race") + if release: + cmd.append("-trimpath") + cmd.append(APP_SRC_DIR) + + try: + subprocess.check_call(cmd, env=env) + except Exception: + print("Failed to build for %s/%s" % (os_name, arch)) + sys.exit(1) + + print("Built %s" % out_name) + + +def cmd_run(args, pprof=False, race=False): + if not check_build_env(): + return + + app_version = get_app_version() + app_date = datetime.datetime.now(datetime.timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) + app_toolchain = get_toolchain() + app_commit = get_app_commit() + lib_version = get_lib_version() + + current_os, current_arch = get_current_os_arch() + + ldflags = [ + "-X", + APP_SRC_CMD_PKG + ".appVersion=" + app_version, + "-X", + APP_SRC_CMD_PKG + ".appDate=" + app_date, + "-X", + APP_SRC_CMD_PKG + ".appType=dev-run", + "-X", + '"' + APP_SRC_CMD_PKG + ".appToolchain=" + app_toolchain + '"', + "-X", + APP_SRC_CMD_PKG + ".appCommit=" + app_commit, + "-X", + APP_SRC_CMD_PKG + ".appPlatform=" + current_os, + "-X", + APP_SRC_CMD_PKG + ".appArch=" + current_arch, + "-X", + APP_SRC_CMD_PKG + ".libVersion=" + lib_version, + ] + + cmd = ["go", "run", "-ldflags", " ".join(ldflags)] + if pprof: + cmd.append("-tags") + cmd.append("pprof") + if race: + cmd.append("-race") + cmd.append(APP_SRC_DIR) + cmd.extend(args) + + try: + subprocess.check_call(cmd) + except KeyboardInterrupt: + pass + except subprocess.CalledProcessError as e: + # Pass through the exit code + sys.exit(e.returncode) + + +def cmd_format(): + if not check_command(["gofumpt", "-version"]): + print("gofumpt is not installed. Please install gofumpt and try again.") + return + + try: + subprocess.check_call(["gofumpt", "-w", "-l", "-extra", "."]) + except Exception: + print("Failed to format code") + + +def cmd_mockgen(): + if not check_command(["mockery", "--version"]): + print("mockery is not installed. Please install mockery and try again.") + return + + for dirpath, dirnames, filenames in os.walk("."): + dirnames[:] = [d for d in dirnames if not d.startswith(".")] + if ".mockery.yaml" in filenames: + print("Generating mocks for %s..." % dirpath) + try: + subprocess.check_call(["mockery"], cwd=dirpath) + except Exception: + print("Failed to generate mocks for %s" % dirpath) + + +def cmd_protogen(): + if not check_command(["protoc", "--version"]): + print("protoc is not installed. Please install protoc and try again.") + return + + for dirpath, dirnames, filenames in os.walk("."): + dirnames[:] = [d for d in dirnames if not d.startswith(".")] + proto_files = [f for f in filenames if f.endswith(".proto")] + + if len(proto_files) > 0: + for proto_file in proto_files: + print("Generating protobuf for %s..." % proto_file) + try: + subprocess.check_call( + ["protoc", "--go_out=paths=source_relative:.", proto_file], + cwd=dirpath, + ) + except Exception: + print("Failed to generate protobuf for %s" % proto_file) + + +def cmd_tidy(): + if not check_build_env(): + return + + for dir in MODULE_SRC_DIRS: + print("Tidying %s..." % dir) + try: + subprocess.check_call(["go", "mod", "tidy"], cwd=dir) + except Exception: + print("Failed to tidy %s" % dir) + + print("Syncing go work...") + try: + subprocess.check_call(["go", "work", "sync"]) + except Exception: + print("Failed to sync go work") + + +def cmd_test(module=None): + if not check_build_env(): + return + + if module: + print("Testing %s..." % module) + try: + subprocess.check_call(["go", "test", "-v", "./..."], cwd=module) + except Exception: + print("Failed to test %s" % module) + else: + for dir in MODULE_SRC_DIRS: + print("Testing %s..." % dir) + try: + subprocess.check_call(["go", "test", "-v", "./..."], cwd=dir) + except Exception: + print("Failed to test %s" % dir) + + +def cmd_publish(urgent=False): + import requests + + if not check_build_env(): + return + + app_version = get_app_version() + app_version_code = get_app_version_code(app_version) + if app_version_code == 0: + print("Invalid app version") + return + + payload = { + "code": app_version_code, + "ver": app_version, + "chan": "release", + "url": "https://github.com/apernet/hysteria/releases", + "urgent": urgent, + } + headers = { + "Content-Type": "application/json", + "Authorization": os.environ.get("HY_API_POST_KEY"), + } + resp = requests.post("https://api.hy2.io/v1/update", json=payload, headers=headers) + + if resp.status_code == 200: + print("Published %s" % app_version) + else: + print("Failed to publish %s, status code: %d" % (app_version, resp.status_code)) + + +def cmd_clean(): + shutil.rmtree(BUILD_DIR, ignore_errors=True) + + +def cmd_about(): + print(LOGO) + print(DESC) + + +def main(): + parser = argparse.ArgumentParser() + + p_cmd = parser.add_subparsers(dest="command") + p_cmd.required = True + + # Run + p_run = p_cmd.add_parser("run", help="Run the app") + p_run.add_argument( + "-p", "--pprof", action="store_true", help="Run with pprof enabled" + ) + p_run.add_argument( + "-d", "--race", action="store_true", help="Build with data race detection" + ) + p_run.add_argument("args", nargs=argparse.REMAINDER) + + # Build + p_build = p_cmd.add_parser("build", help="Build the app") + p_build.add_argument( + "-p", "--pprof", action="store_true", help="Build with pprof enabled" + ) + p_build.add_argument( + "-r", "--release", action="store_true", help="Build a release version" + ) + p_build.add_argument( + "-d", "--race", action="store_true", help="Build with data race detection" + ) + + # Format + p_cmd.add_parser("format", help="Format the code") + + # Mockgen + p_cmd.add_parser("mockgen", help="Generate mock interfaces") + + # Protogen + p_cmd.add_parser("protogen", help="Generate protobuf interfaces") + + # Tidy + p_cmd.add_parser("tidy", help="Tidy the go modules") + + # Test + p_test = p_cmd.add_parser("test", help="Test the code") + p_test.add_argument("module", nargs="?", help="Module to test") + + # Publish + p_pub = p_cmd.add_parser("publish", help="Publish the current version") + p_pub.add_argument( + "-u", "--urgent", action="store_true", help="Publish as an urgent update" + ) + + # Clean + p_cmd.add_parser("clean", help="Clean the build directory") + + # About + p_cmd.add_parser("about", help="Print about information") + + args = parser.parse_args() + + if args.command == "run": + cmd_run(args.args, args.pprof, args.race) + elif args.command == "build": + cmd_build(args.pprof, args.release, args.race) + elif args.command == "format": + cmd_format() + elif args.command == "mockgen": + cmd_mockgen() + elif args.command == "protogen": + cmd_protogen() + elif args.command == "tidy": + cmd_tidy() + elif args.command == "test": + cmd_test(args.module) + elif args.command == "publish": + cmd_publish(args.urgent) + elif args.command == "clean": + cmd_clean() + elif args.command == "about": + cmd_about() + + +if __name__ == "__main__": + main() diff --git a/v2rayng/hysteria/logo.svg b/v2rayng/hysteria/logo.svg new file mode 100644 index 0000000000..9c85c9ca07 --- /dev/null +++ b/v2rayng/hysteria/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/media-kit/png/black 1@2x.png b/v2rayng/hysteria/media-kit/png/black 1@2x.png new file mode 100644 index 0000000000..a7169fe900 Binary files /dev/null and b/v2rayng/hysteria/media-kit/png/black 1@2x.png differ diff --git a/v2rayng/hysteria/media-kit/png/black 2@2x.png b/v2rayng/hysteria/media-kit/png/black 2@2x.png new file mode 100644 index 0000000000..2345a74465 Binary files /dev/null and b/v2rayng/hysteria/media-kit/png/black 2@2x.png differ diff --git a/v2rayng/hysteria/media-kit/png/black 3@2x.png b/v2rayng/hysteria/media-kit/png/black 3@2x.png new file mode 100644 index 0000000000..71388a84e5 Binary files /dev/null and b/v2rayng/hysteria/media-kit/png/black 3@2x.png differ diff --git a/v2rayng/hysteria/media-kit/png/black 4@2x.png b/v2rayng/hysteria/media-kit/png/black 4@2x.png new file mode 100644 index 0000000000..1e6d36ddea Binary files /dev/null and b/v2rayng/hysteria/media-kit/png/black 4@2x.png differ diff --git a/v2rayng/hysteria/media-kit/png/dark bg 1@2x.png b/v2rayng/hysteria/media-kit/png/dark bg 1@2x.png new file mode 100644 index 0000000000..bfa9aef09e Binary files /dev/null and b/v2rayng/hysteria/media-kit/png/dark bg 1@2x.png differ diff --git a/v2rayng/hysteria/media-kit/png/dark bg 2@2x.png b/v2rayng/hysteria/media-kit/png/dark bg 2@2x.png new file mode 100644 index 0000000000..66f5a3366f Binary files /dev/null and b/v2rayng/hysteria/media-kit/png/dark bg 2@2x.png differ diff --git a/v2rayng/hysteria/media-kit/png/dark bg 3@2x.png b/v2rayng/hysteria/media-kit/png/dark bg 3@2x.png new file mode 100644 index 0000000000..21aab76e48 Binary files /dev/null and b/v2rayng/hysteria/media-kit/png/dark bg 3@2x.png differ diff --git a/v2rayng/hysteria/media-kit/png/dark bg 4@2x.png b/v2rayng/hysteria/media-kit/png/dark bg 4@2x.png new file mode 100644 index 0000000000..23373aaa66 Binary files /dev/null and b/v2rayng/hysteria/media-kit/png/dark bg 4@2x.png differ diff --git a/v2rayng/hysteria/media-kit/png/light bg 1@2x.png b/v2rayng/hysteria/media-kit/png/light bg 1@2x.png new file mode 100644 index 0000000000..08cb25dd95 Binary files /dev/null and b/v2rayng/hysteria/media-kit/png/light bg 1@2x.png differ diff --git a/v2rayng/hysteria/media-kit/png/light bg 2@2x.png b/v2rayng/hysteria/media-kit/png/light bg 2@2x.png new file mode 100644 index 0000000000..0b38481efa Binary files /dev/null and b/v2rayng/hysteria/media-kit/png/light bg 2@2x.png differ diff --git a/v2rayng/hysteria/media-kit/png/light bg 3@2x.png b/v2rayng/hysteria/media-kit/png/light bg 3@2x.png new file mode 100644 index 0000000000..bdf40df1d0 Binary files /dev/null and b/v2rayng/hysteria/media-kit/png/light bg 3@2x.png differ diff --git a/v2rayng/hysteria/media-kit/png/light bg 4@2x.png b/v2rayng/hysteria/media-kit/png/light bg 4@2x.png new file mode 100644 index 0000000000..7aa79d40f1 Binary files /dev/null and b/v2rayng/hysteria/media-kit/png/light bg 4@2x.png differ diff --git a/v2rayng/hysteria/media-kit/png/symbol 1@2x.png b/v2rayng/hysteria/media-kit/png/symbol 1@2x.png new file mode 100644 index 0000000000..31ee7f1f3c Binary files /dev/null and b/v2rayng/hysteria/media-kit/png/symbol 1@2x.png differ diff --git a/v2rayng/hysteria/media-kit/png/symbol 2@2x.png b/v2rayng/hysteria/media-kit/png/symbol 2@2x.png new file mode 100644 index 0000000000..54d1c48d16 Binary files /dev/null and b/v2rayng/hysteria/media-kit/png/symbol 2@2x.png differ diff --git a/v2rayng/hysteria/media-kit/png/symbol 3@2x.png b/v2rayng/hysteria/media-kit/png/symbol 3@2x.png new file mode 100644 index 0000000000..e0af9bb3c1 Binary files /dev/null and b/v2rayng/hysteria/media-kit/png/symbol 3@2x.png differ diff --git a/v2rayng/hysteria/media-kit/png/symbol 4@2x.png b/v2rayng/hysteria/media-kit/png/symbol 4@2x.png new file mode 100644 index 0000000000..608f2704f9 Binary files /dev/null and b/v2rayng/hysteria/media-kit/png/symbol 4@2x.png differ diff --git a/v2rayng/hysteria/media-kit/png/white 1@2x.png b/v2rayng/hysteria/media-kit/png/white 1@2x.png new file mode 100644 index 0000000000..6ffce42601 Binary files /dev/null and b/v2rayng/hysteria/media-kit/png/white 1@2x.png differ diff --git a/v2rayng/hysteria/media-kit/png/white 2@2x.png b/v2rayng/hysteria/media-kit/png/white 2@2x.png new file mode 100644 index 0000000000..fa818f672c Binary files /dev/null and b/v2rayng/hysteria/media-kit/png/white 2@2x.png differ diff --git a/v2rayng/hysteria/media-kit/png/white 3@2x.png b/v2rayng/hysteria/media-kit/png/white 3@2x.png new file mode 100644 index 0000000000..f9b2307edc Binary files /dev/null and b/v2rayng/hysteria/media-kit/png/white 3@2x.png differ diff --git a/v2rayng/hysteria/media-kit/png/white 4@2x.png b/v2rayng/hysteria/media-kit/png/white 4@2x.png new file mode 100644 index 0000000000..fe6389681a Binary files /dev/null and b/v2rayng/hysteria/media-kit/png/white 4@2x.png differ diff --git a/v2rayng/hysteria/media-kit/svg/black 1.svg b/v2rayng/hysteria/media-kit/svg/black 1.svg new file mode 100644 index 0000000000..57e1a7e8a6 --- /dev/null +++ b/v2rayng/hysteria/media-kit/svg/black 1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/media-kit/svg/black 2.svg b/v2rayng/hysteria/media-kit/svg/black 2.svg new file mode 100644 index 0000000000..0d3228a934 --- /dev/null +++ b/v2rayng/hysteria/media-kit/svg/black 2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/media-kit/svg/black 3.svg b/v2rayng/hysteria/media-kit/svg/black 3.svg new file mode 100644 index 0000000000..8665fcd388 --- /dev/null +++ b/v2rayng/hysteria/media-kit/svg/black 3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/media-kit/svg/black 4.svg b/v2rayng/hysteria/media-kit/svg/black 4.svg new file mode 100644 index 0000000000..e8f592176f --- /dev/null +++ b/v2rayng/hysteria/media-kit/svg/black 4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/media-kit/svg/dark bg 1.svg b/v2rayng/hysteria/media-kit/svg/dark bg 1.svg new file mode 100644 index 0000000000..b7712d6511 --- /dev/null +++ b/v2rayng/hysteria/media-kit/svg/dark bg 1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/media-kit/svg/dark bg 2.svg b/v2rayng/hysteria/media-kit/svg/dark bg 2.svg new file mode 100644 index 0000000000..471ad15705 --- /dev/null +++ b/v2rayng/hysteria/media-kit/svg/dark bg 2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/media-kit/svg/dark bg 3.svg b/v2rayng/hysteria/media-kit/svg/dark bg 3.svg new file mode 100644 index 0000000000..e40ee59f1e --- /dev/null +++ b/v2rayng/hysteria/media-kit/svg/dark bg 3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/media-kit/svg/dark bg 4.svg b/v2rayng/hysteria/media-kit/svg/dark bg 4.svg new file mode 100644 index 0000000000..ef00329b75 --- /dev/null +++ b/v2rayng/hysteria/media-kit/svg/dark bg 4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/media-kit/svg/light bg 1.svg b/v2rayng/hysteria/media-kit/svg/light bg 1.svg new file mode 100644 index 0000000000..37a1dede8d --- /dev/null +++ b/v2rayng/hysteria/media-kit/svg/light bg 1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/media-kit/svg/light bg 2.svg b/v2rayng/hysteria/media-kit/svg/light bg 2.svg new file mode 100644 index 0000000000..3adb2204a6 --- /dev/null +++ b/v2rayng/hysteria/media-kit/svg/light bg 2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/media-kit/svg/light bg 3.svg b/v2rayng/hysteria/media-kit/svg/light bg 3.svg new file mode 100644 index 0000000000..9c85c9ca07 --- /dev/null +++ b/v2rayng/hysteria/media-kit/svg/light bg 3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/media-kit/svg/light bg 4.svg b/v2rayng/hysteria/media-kit/svg/light bg 4.svg new file mode 100644 index 0000000000..d443e6fa2b --- /dev/null +++ b/v2rayng/hysteria/media-kit/svg/light bg 4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/media-kit/svg/symbol 1.svg b/v2rayng/hysteria/media-kit/svg/symbol 1.svg new file mode 100644 index 0000000000..912747b71a --- /dev/null +++ b/v2rayng/hysteria/media-kit/svg/symbol 1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/media-kit/svg/symbol 2.svg b/v2rayng/hysteria/media-kit/svg/symbol 2.svg new file mode 100644 index 0000000000..2549ac2254 --- /dev/null +++ b/v2rayng/hysteria/media-kit/svg/symbol 2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/media-kit/svg/symbol 3.svg b/v2rayng/hysteria/media-kit/svg/symbol 3.svg new file mode 100644 index 0000000000..3646150866 --- /dev/null +++ b/v2rayng/hysteria/media-kit/svg/symbol 3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/media-kit/svg/symbol 4.svg b/v2rayng/hysteria/media-kit/svg/symbol 4.svg new file mode 100644 index 0000000000..f0c503592e --- /dev/null +++ b/v2rayng/hysteria/media-kit/svg/symbol 4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/media-kit/svg/white 1.svg b/v2rayng/hysteria/media-kit/svg/white 1.svg new file mode 100644 index 0000000000..e0aa37f06d --- /dev/null +++ b/v2rayng/hysteria/media-kit/svg/white 1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/media-kit/svg/white 2.svg b/v2rayng/hysteria/media-kit/svg/white 2.svg new file mode 100644 index 0000000000..cf5a27d3bc --- /dev/null +++ b/v2rayng/hysteria/media-kit/svg/white 2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/media-kit/svg/white 3.svg b/v2rayng/hysteria/media-kit/svg/white 3.svg new file mode 100644 index 0000000000..01d5795459 --- /dev/null +++ b/v2rayng/hysteria/media-kit/svg/white 3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/media-kit/svg/white 4.svg b/v2rayng/hysteria/media-kit/svg/white 4.svg new file mode 100644 index 0000000000..d69216b252 --- /dev/null +++ b/v2rayng/hysteria/media-kit/svg/white 4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v2rayng/hysteria/platforms.txt b/v2rayng/hysteria/platforms.txt new file mode 100644 index 0000000000..ea0ddf3aba --- /dev/null +++ b/v2rayng/hysteria/platforms.txt @@ -0,0 +1,37 @@ +# This file controls what platform/architecture combinations we build for a release. + +# Windows +windows/amd64 +windows/amd64-avx +windows/386 +windows/arm64 + +# macOS +darwin/amd64 +darwin/amd64-avx +darwin/arm64 + +# Linux +linux/amd64 +linux/amd64-avx +linux/386 +linux/arm +linux/armv5 +linux/arm64 +linux/s390x +linux/mipsle +linux/mipsle-sf +linux/riscv64 + +# Android +android/386 +android/amd64 +android/armv7 +android/arm64 + +# FreeBSD +freebsd/amd64 +freebsd/amd64-avx +freebsd/386 +freebsd/arm +freebsd/arm64 diff --git a/v2rayng/hysteria/requirements.txt b/v2rayng/hysteria/requirements.txt new file mode 100644 index 0000000000..44ee651ae0 --- /dev/null +++ b/v2rayng/hysteria/requirements.txt @@ -0,0 +1,11 @@ +blinker==1.8.2 +cffi==1.17.0 +click==8.1.7 +cryptography==43.0.0 +Flask==3.0.3 +itsdangerous==2.2.0 +Jinja2==3.1.4 +MarkupSafe==2.1.5 +pycparser==2.22 +PySocks==1.7.1 +Werkzeug==3.0.4 diff --git a/v2rayng/hysteria/scripts/_redirects b/v2rayng/hysteria/scripts/_redirects new file mode 100644 index 0000000000..3841e90bdf --- /dev/null +++ b/v2rayng/hysteria/scripts/_redirects @@ -0,0 +1 @@ +/ /install_server.sh 301 diff --git a/v2rayng/hysteria/scripts/install_server.sh b/v2rayng/hysteria/scripts/install_server.sh new file mode 100644 index 0000000000..4277ab946b --- /dev/null +++ b/v2rayng/hysteria/scripts/install_server.sh @@ -0,0 +1,1167 @@ +#!/usr/bin/env bash +# +# install_server.sh - hysteria server install script +# Try `install_server.sh --help` for usage. +# +# SPDX-License-Identifier: MIT +# Copyright (c) 2023 Aperture Internet Laboratory +# + +set -e + + +### +# SCRIPT CONFIGURATION +### + +# Basename of this script +SCRIPT_NAME="$(basename "$0")" + +# Command line arguments of this script +SCRIPT_ARGS=("$@") + +# Path for installing executable +EXECUTABLE_INSTALL_PATH="/usr/local/bin/hysteria" + +# Paths to install systemd files +SYSTEMD_SERVICES_DIR="/etc/systemd/system" + +# Directory to store hysteria config file +CONFIG_DIR="/etc/hysteria" + +# URLs of GitHub +REPO_URL="https://github.com/apernet/hysteria" + +# URL of Hysteria 2 API +HY2_API_BASE_URL="https://api.hy2.io/v1" + +# curl command line flags. +# To using a proxy, please specify ALL_PROXY in the environ variable, such like: +# export ALL_PROXY=socks5h://192.0.2.1:1080 +CURL_FLAGS=(-L -f -q --retry 5 --retry-delay 10 --retry-max-time 60) + + +### +# AUTO DETECTED GLOBAL VARIABLE +### + +# Package manager +PACKAGE_MANAGEMENT_INSTALL="${PACKAGE_MANAGEMENT_INSTALL:-}" + +# Operating System of current machine, supported: linux +OPERATING_SYSTEM="${OPERATING_SYSTEM:-}" + +# Architecture of current machine, supported: 386, amd64, arm, arm64, mipsle, s390x +ARCHITECTURE="${ARCHITECTURE:-}" + +# User for running hysteria +HYSTERIA_USER="${HYSTERIA_USER:-}" + +# Directory for ACME certificates storage +HYSTERIA_HOME_DIR="${HYSTERIA_HOME_DIR:-}" + +# SELinux context of systemd unit files +SECONTEXT_SYSTEMD_UNIT="${SECONTEXT_SYSTEMD_UNIT:-}" + + +### +# ARGUMENTS +### + +# Supported operation: install, remove, check_update +OPERATION= + +# User specified version to install +VERSION= + +# Force install even if installed +FORCE= + +# User specified binary to install +LOCAL_FILE= + + +### +# COMMAND REPLACEMENT & UTILITIES +### + +has_command() { + local _command=$1 + + type -P "$_command" > /dev/null 2>&1 +} + +curl() { + command curl "${CURL_FLAGS[@]}" "$@" +} + +mktemp() { + command mktemp "$@" "/tmp/hyservinst.XXXXXXXXXX" +} + +tput() { + if has_command tput; then + command tput "$@" + fi +} + +tred() { + tput setaf 1 +} + +tgreen() { + tput setaf 2 +} + +tyellow() { + tput setaf 3 +} + +tblue() { + tput setaf 4 +} + +taoi() { + tput setaf 6 +} + +tbold() { + tput bold +} + +treset() { + tput sgr0 +} + +note() { + local _msg="$1" + + echo -e "$SCRIPT_NAME: $(tbold)note: $_msg$(treset)" +} + +warning() { + local _msg="$1" + + echo -e "$SCRIPT_NAME: $(tyellow)warning: $_msg$(treset)" +} + +error() { + local _msg="$1" + + echo -e "$SCRIPT_NAME: $(tred)error: $_msg$(treset)" +} + +has_prefix() { + local _s="$1" + local _prefix="$2" + + if [[ -z "$_prefix" ]]; then + return 0 + fi + + if [[ -z "$_s" ]]; then + return 1 + fi + + [[ "x$_s" != "x${_s#"$_prefix"}" ]] +} + +generate_random_password() { + dd if=/dev/random bs=18 count=1 status=none | base64 +} + +systemctl() { + if [[ "x$FORCE_NO_SYSTEMD" == "x2" ]] || ! has_command systemctl; then + warning "Ignored systemd command: systemctl $@" + return + fi + + command systemctl "$@" +} + +chcon() { + if ! has_command chcon || [[ "x$FORCE_NO_SELINUX" == "x1" ]]; then + return + fi + + command chcon "$@" +} + +get_systemd_version() { + if ! has_command systemctl; then + return + fi + + command systemctl --version | head -1 | cut -d ' ' -f 2 +} + +systemd_unit_working_directory() { + local _systemd_version="$(get_systemd_version || true)" + + # WorkingDirectory=~ requires systemd v227 or later. + # (released on Oct 2015, only CentOS 7 use an earlier version) + # ref: systemd/systemd@5f5d8eab1f2f5f5e088bc301533b3e4636de96c7 + if [[ -n "$_systemd_version" && "$_systemd_version" -lt "227" ]]; then + echo "$HYSTERIA_HOME_DIR" + return + fi + + echo "~" +} + +get_selinux_context() { + local _file="$1" + + local _lsres="$(ls -dZ "$_file" | head -1)" + local _sectx='' + case "$(echo "$_lsres" | wc -w)" in + 2) + _sectx="$(echo "$_lsres" | cut -d ' ' -f 1)" + ;; + 5) + _sectx="$(echo "$_lsres" | cut -d ' ' -f 4)" + ;; + *) + ;; + esac + + if [[ "x$_sectx" == "x?" ]]; then + _sectx="" + fi + + echo "$_sectx" +} + +show_argument_error_and_exit() { + local _error_msg="$1" + + error "$_error_msg" + echo "Try \"$0 --help\" for usage." >&2 + exit 22 +} + +install_content() { + local _install_flags="$1" + local _content="$2" + local _destination="$3" + local _overwrite="$4" + + local _tmpfile="$(mktemp)" + + echo -ne "Install $_destination ... " + echo "$_content" > "$_tmpfile" + if [[ -z "$_overwrite" && -e "$_destination" ]]; then + echo -e "exists" + elif install "$_install_flags" "$_tmpfile" "$_destination"; then + echo -e "ok" + fi + + rm -f "$_tmpfile" +} + +remove_file() { + local _target="$1" + + echo -ne "Remove $_target ... " + if rm "$_target"; then + echo -e "ok" + fi +} + +exec_sudo() { + # exec sudo with configurable environ preserved. + local _saved_ifs="$IFS" + IFS=$'\n' + local _preserved_env=( + $(env | grep "^PACKAGE_MANAGEMENT_INSTALL=" || true) + $(env | grep "^OPERATING_SYSTEM=" || true) + $(env | grep "^ARCHITECTURE=" || true) + $(env | grep "^HYSTERIA_\w*=" || true) + $(env | grep "^SECONTEXT_SYSTEMD_UNIT=" || true) + $(env | grep "^FORCE_\w*=" || true) + ) + IFS="$_saved_ifs" + + exec sudo env \ + "${_preserved_env[@]}" \ + "$@" +} + +detect_package_manager() { + if [[ -n "$PACKAGE_MANAGEMENT_INSTALL" ]]; then + return 0 + fi + + if has_command apt; then + apt update + PACKAGE_MANAGEMENT_INSTALL='apt -y --no-install-recommends install' + return 0 + fi + + if has_command dnf; then + PACKAGE_MANAGEMENT_INSTALL='dnf -y install' + return 0 + fi + + if has_command yum; then + PACKAGE_MANAGEMENT_INSTALL='yum -y install' + return 0 + fi + + if has_command zypper; then + PACKAGE_MANAGEMENT_INSTALL='zypper install -y --no-recommends' + return 0 + fi + + if has_command pacman; then + PACKAGE_MANAGEMENT_INSTALL='pacman -Syu --noconfirm' + return 0 + fi + + return 1 +} + +install_software() { + local _package_name="$1" + + if ! detect_package_manager; then + error "Supported package manager is not detected, please install the following package manually:" + echo + echo -e "\t* $_package_name" + echo + exit 65 + fi + + echo "Installing missing dependence '$_package_name' with '$PACKAGE_MANAGEMENT_INSTALL' ... " + if $PACKAGE_MANAGEMENT_INSTALL "$_package_name"; then + echo "ok" + else + error "Cannot install '$_package_name' with detected package manager, please install it manually." + exit 65 + fi +} + +is_user_exists() { + local _user="$1" + + id "$_user" > /dev/null 2>&1 +} + +rerun_with_sudo() { + if ! has_command sudo; then + return 13 + fi + + local _target_script + + if has_prefix "$0" "/dev/" || has_prefix "$0" "/proc/"; then + local _tmp_script="$(mktemp)" + chmod +x "$_tmp_script" + + if has_command curl; then + curl -o "$_tmp_script" 'https://get.hy2.sh/' + elif has_command wget; then + wget -O "$_tmp_script" 'https://get.hy2.sh' + else + return 127 + fi + + _target_script="$_tmp_script" + else + _target_script="$0" + fi + + note "Re-running this script with sudo. You can also specify FORCE_NO_ROOT=1 to force this script to run as the current user." + exec_sudo "$_target_script" "${SCRIPT_ARGS[@]}" +} + +check_permission() { + if [[ "$UID" -eq '0' ]]; then + return + fi + + note "The user running this script is not root." + + case "$FORCE_NO_ROOT" in + '1') + warning "FORCE_NO_ROOT=1 detected, we will proceed without root, but you may get insufficient privileges errors." + ;; + *) + if ! rerun_with_sudo; then + error "Please run this script with root or specify FORCE_NO_ROOT=1 to force this script to run as the current user." + exit 13 + fi + ;; + esac +} + +check_environment_operating_system() { + if [[ -n "$OPERATING_SYSTEM" ]]; then + warning "OPERATING_SYSTEM=$OPERATING_SYSTEM detected, operating system detection will not be performed." + return + fi + + if [[ "x$(uname)" == "xLinux" ]]; then + OPERATING_SYSTEM=linux + return + fi + + error "This script only supports Linux." + note "Specify OPERATING_SYSTEM=[linux|darwin|freebsd|windows] to bypass this check and force this script to run on this $(uname)." + exit 95 +} + +check_environment_architecture() { + if [[ -n "$ARCHITECTURE" ]]; then + warning "ARCHITECTURE=$ARCHITECTURE detected, architecture detection will not be performed." + return + fi + + case "$(uname -m)" in + 'i386' | 'i686') + ARCHITECTURE='386' + ;; + 'amd64' | 'x86_64') + ARCHITECTURE='amd64' + ;; + 'armv5tel' | 'armv6l' | 'armv7' | 'armv7l') + ARCHITECTURE='arm' + ;; + 'armv8' | 'aarch64') + ARCHITECTURE='arm64' + ;; + 'mips' | 'mipsle' | 'mips64' | 'mips64le') + ARCHITECTURE='mipsle' + ;; + 's390x') + ARCHITECTURE='s390x' + ;; + *) + error "The architecture '$(uname -a)' is not supported." + note "Specify ARCHITECTURE= to bypass this check and force this script to run on this $(uname -m)." + exit 8 + ;; + esac +} + +check_environment_systemd() { + if [[ -d "/run/systemd/system" ]] || grep -q systemd <(ls -l /sbin/init); then + return + fi + + case "$FORCE_NO_SYSTEMD" in + '1') + warning "FORCE_NO_SYSTEMD=1, we will proceed as normal even if systemd is not detected." + ;; + '2') + warning "FORCE_NO_SYSTEMD=2, we will proceed but skip all systemd related commands." + ;; + *) + error "This script only supports Linux distributions with systemd." + note "Specify FORCE_NO_SYSTEMD=1 to disable this check and force this script to run as if systemd exists." + note "Specify FORCE_NO_SYSTEMD=2 to disable this check and skip all systemd related commands." + ;; + esac +} + +check_environment_selinux() { + if ! has_command getenforce; then + return + fi + + note "SELinux is detected" + + if [[ "x$FORCE_NO_SELINUX" == "x1" ]]; then + warning "FORCE_NO_SELINUX=1, we will skip all SELinux related commands." + return + fi + + if [[ -z "$SECONTEXT_SYSTEMD_UNIT" ]]; then + if [[ -z "$FORCE_NO_SYSTEMD" ]] && [[ -e "$SYSTEMD_SERVICES_DIR" ]]; then + local _sectx="$(get_selinux_context "$SYSTEMD_SERVICES_DIR")" + if [[ -z "$_sectx" ]]; then + warning "Failed to obtain SEContext of $SYSTEMD_SERVICES_DIR" + else + SECONTEXT_SYSTEMD_UNIT="$_sectx" + fi + fi + fi +} + +check_environment_curl() { + if has_command curl; then + return + fi + + install_software curl +} + +check_environment_grep() { + if has_command grep; then + return + fi + + install_software grep +} + +check_environment() { + check_environment_operating_system + check_environment_architecture + check_environment_systemd + check_environment_selinux + check_environment_curl + check_environment_grep +} + +vercmp_segment() { + local _lhs="$1" + local _rhs="$2" + + if [[ "x$_lhs" == "x$_rhs" ]]; then + echo 0 + return + fi + if [[ -z "$_lhs" ]]; then + echo -1 + return + fi + if [[ -z "$_rhs" ]]; then + echo 1 + return + fi + + local _lhs_num="${_lhs//[A-Za-z]*/}" + local _rhs_num="${_rhs//[A-Za-z]*/}" + + if [[ "x$_lhs_num" == "x$_rhs_num" ]]; then + echo 0 + return + fi + if [[ -z "$_lhs_num" ]]; then + echo -1 + return + fi + if [[ -z "$_rhs_num" ]]; then + echo 1 + return + fi + local _numcmp=$(($_lhs_num - $_rhs_num)) + if [[ "$_numcmp" -ne 0 ]]; then + echo "$_numcmp" + return + fi + + local _lhs_suffix="${_lhs#"$_lhs_num"}" + local _rhs_suffix="${_rhs#"$_rhs_num"}" + + if [[ "x$_lhs_suffix" == "x$_rhs_suffix" ]]; then + echo 0 + return + fi + if [[ -z "$_lhs_suffix" ]]; then + echo 1 + return + fi + if [[ -z "$_rhs_suffix" ]]; then + echo -1 + return + fi + if [[ "$_lhs_suffix" < "$_rhs_suffix" ]]; then + echo -1 + return + fi + echo 1 +} + +vercmp() { + local _lhs=${1#v} + local _rhs=${2#v} + + while [[ -n "$_lhs" && -n "$_rhs" ]]; do + local _clhs="${_lhs/.*/}" + local _crhs="${_rhs/.*/}" + + local _segcmp="$(vercmp_segment "$_clhs" "$_crhs")" + if [[ "$_segcmp" -ne 0 ]]; then + echo "$_segcmp" + return + fi + + _lhs="${_lhs#"$_clhs"}" + _lhs="${_lhs#.}" + _rhs="${_rhs#"$_crhs"}" + _rhs="${_rhs#.}" + done + + if [[ "x$_lhs" == "x$_rhs" ]]; then + echo 0 + return + fi + + if [[ -z "$_lhs" ]]; then + echo -1 + return + fi + + if [[ -z "$_rhs" ]]; then + echo 1 + return + fi + + return +} + +check_hysteria_user() { + local _default_hysteria_user="$1" + + if [[ -n "$HYSTERIA_USER" ]]; then + return + fi + + if [[ ! -e "$SYSTEMD_SERVICES_DIR/hysteria-server.service" ]]; then + HYSTERIA_USER="$_default_hysteria_user" + return + fi + + HYSTERIA_USER="$(grep -o '^User=\w*' "$SYSTEMD_SERVICES_DIR/hysteria-server.service" | tail -1 | cut -d '=' -f 2 || true)" + + if [[ -z "$HYSTERIA_USER" ]]; then + HYSTERIA_USER="$_default_hysteria_user" + fi +} + +check_hysteria_homedir() { + local _default_hysteria_homedir="$1" + + if [[ -n "$HYSTERIA_HOME_DIR" ]]; then + return + fi + + if ! is_user_exists "$HYSTERIA_USER"; then + HYSTERIA_HOME_DIR="$_default_hysteria_homedir" + return + fi + + HYSTERIA_HOME_DIR="$(eval echo ~"$HYSTERIA_USER")" +} + + +### +# ARGUMENTS PARSER +### + +show_usage_and_exit() { + echo + echo -e "\t$(tbold)$SCRIPT_NAME$(treset) - hysteria server install script" + echo + echo -e "Usage:" + echo + echo -e "$(tbold)Install hysteria$(treset)" + echo -e "\t$0 [ -f | -l | --version ]" + echo -e "Flags:" + echo -e "\t-f, --force\tForce re-install latest or specified version even if it has been installed." + echo -e "\t-l, --local \tInstall specified hysteria binary instead of download it." + echo -e "\t--version \tInstall specified version instead of the latest." + echo + echo -e "$(tbold)Remove hysteria$(treset)" + echo -e "\t$0 --remove" + echo + echo -e "$(tbold)Check for the update$(treset)" + echo -e "\t$0 -c" + echo -e "\t$0 --check" + echo + echo -e "$(tbold)Show this help$(treset)" + echo -e "\t$0 -h" + echo -e "\t$0 --help" + exit 0 +} + +parse_arguments() { + while [[ "$#" -gt '0' ]]; do + case "$1" in + '--remove') + if [[ -n "$OPERATION" && "$OPERATION" != 'remove' ]]; then + show_argument_error_and_exit "Option '--remove' is in conflict with other options." + fi + OPERATION='remove' + ;; + '--version') + VERSION="$2" + if [[ -z "$VERSION" ]]; then + show_argument_error_and_exit "Please specify the version for option '--version'." + fi + shift + if ! has_prefix "$VERSION" 'v'; then + show_argument_error_and_exit "Version numbers should begin with 'v' (such as 'v2.0.0'), got '$VERSION'" + fi + ;; + '-c' | '--check') + if [[ -n "$OPERATION" && "$OPERATION" != 'check' ]]; then + show_argument_error_and_exit "Option '-c' or '--check' is in conflict with other options." + fi + OPERATION='check_update' + ;; + '-f' | '--force') + FORCE='1' + ;; + '-h' | '--help') + show_usage_and_exit + ;; + '-l' | '--local') + LOCAL_FILE="$2" + if [[ -z "$LOCAL_FILE" ]]; then + show_argument_error_and_exit "Please specify the local binary to install for option '-l' or '--local'." + fi + break + ;; + *) + show_argument_error_and_exit "Unknown option '$1'" + ;; + esac + shift + done + + if [[ -z "$OPERATION" ]]; then + OPERATION='install' + fi + + # validate arguments + case "$OPERATION" in + 'install') + if [[ -n "$VERSION" && -n "$LOCAL_FILE" ]]; then + show_argument_error_and_exit '--version and --local cannot be used together.' + fi + ;; + *) + if [[ -n "$VERSION" ]]; then + show_argument_error_and_exit "--version is only valid for install operation." + fi + if [[ -n "$LOCAL_FILE" ]]; then + show_argument_error_and_exit "--local is only valid for install operation." + fi + ;; + esac +} + + +### +# FILE TEMPLATES +### + +# /etc/systemd/system/hysteria-server.service +tpl_hysteria_server_service_base() { + local _config_name="$1" + + cat << EOF +[Unit] +Description=Hysteria Server Service (${_config_name}.yaml) +After=network.target + +[Service] +Type=simple +ExecStart=$EXECUTABLE_INSTALL_PATH server --config ${CONFIG_DIR}/${_config_name}.yaml +WorkingDirectory=$(systemd_unit_working_directory) +User=$HYSTERIA_USER +Group=$HYSTERIA_USER +Environment=HYSTERIA_LOG_LEVEL=info +CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW +AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW +NoNewPrivileges=true + +[Install] +WantedBy=multi-user.target +EOF +} + +# /etc/systemd/system/hysteria-server.service +tpl_hysteria_server_service() { + tpl_hysteria_server_service_base 'config' +} + +# /etc/systemd/system/hysteria-server@.service +tpl_hysteria_server_x_service() { + tpl_hysteria_server_service_base '%i' +} + +# /etc/hysteria/config.yaml +tpl_etc_hysteria_config_yaml() { + cat << EOF +# listen: :443 + +acme: + domains: + - your.domain.net + email: your@email.com + +auth: + type: password + password: $(generate_random_password) + +masquerade: + type: proxy + proxy: + url: https://news.ycombinator.com/ + rewriteHost: true +EOF +} + + +### +# SYSTEMD +### + +get_running_services() { + if [[ "x$FORCE_NO_SYSTEMD" == "x2" ]]; then + return + fi + + systemctl list-units --state=active --plain --no-legend \ + | grep -o "hysteria-server@*[^\s]*.service" || true +} + +restart_running_services() { + if [[ "x$FORCE_NO_SYSTEMD" == "x2" ]]; then + return + fi + + echo "Restarting running service ... " + + for service in $(get_running_services); do + echo -ne "Restarting $service ... " + systemctl restart "$service" + echo "done" + done +} + +stop_running_services() { + if [[ "x$FORCE_NO_SYSTEMD" == "x2" ]]; then + return + fi + + echo "Stopping running service ... " + + for service in $(get_running_services); do + echo -ne "Stopping $service ... " + systemctl stop "$service" + echo "done" + done +} + + +### +# HYSTERIA & GITHUB API +### + +is_hysteria_installed() { + # RETURN VALUE + # 0: hysteria is installed + # 1: hysteria is not installed + + if [[ -f "$EXECUTABLE_INSTALL_PATH" || -h "$EXECUTABLE_INSTALL_PATH" ]]; then + return 0 + fi + return 1 +} + +is_hysteria1_version() { + local _version="$1" + + has_prefix "$_version" "v1." || has_prefix "$_version" "v0." +} + +get_installed_version() { + if is_hysteria_installed; then + if "$EXECUTABLE_INSTALL_PATH" version > /dev/null 2>&1; then + "$EXECUTABLE_INSTALL_PATH" version | grep '^Version' | grep -o 'v[.0-9]*' + elif "$EXECUTABLE_INSTALL_PATH" -v > /dev/null 2>&1; then + # hysteria 1 + "$EXECUTABLE_INSTALL_PATH" -v | cut -d ' ' -f 3 + fi + fi +} + +get_latest_version() { + if [[ -n "$VERSION" ]]; then + echo "$VERSION" + return + fi + + local _tmpfile=$(mktemp) + if ! curl -sS "$HY2_API_BASE_URL/update?cver=installscript&plat=${OPERATING_SYSTEM}&arch=${ARCHITECTURE}&chan=release&side=server" -o "$_tmpfile"; then + error "Failed to get the latest version from Hysteria 2 API, please check your network and try again." + exit 11 + fi + + local _latest_version=$(grep -oP '"lver":\s*\K"v.*?"' "$_tmpfile" | head -1) + _latest_version=${_latest_version#'"'} + _latest_version=${_latest_version%'"'} + + if [[ -n "$_latest_version" ]]; then + echo "$_latest_version" + fi + + rm -f "$_tmpfile" +} + +download_hysteria() { + local _version="$1" + local _destination="$2" + + local _download_url="$REPO_URL/releases/download/app/$_version/hysteria-$OPERATING_SYSTEM-$ARCHITECTURE" + echo "Downloading hysteria binary: $_download_url ..." + if ! curl -R -H 'Cache-Control: no-cache' "$_download_url" -o "$_destination"; then + error "Download failed, please check your network and try again." + return 11 + fi + return 0 +} + +check_update() { + # RETURN VALUE + # 0: update available + # 1: installed version is latest + + echo -ne "Checking for installed version ... " + local _installed_version="$(get_installed_version)" + if [[ -n "$_installed_version" ]]; then + echo "$_installed_version" + else + echo "not installed" + fi + + echo -ne "Checking for latest version ... " + local _latest_version="$(get_latest_version)" + if [[ -n "$_latest_version" ]]; then + echo "$_latest_version" + VERSION="$_latest_version" + else + echo "failed" + return 1 + fi + + local _vercmp="$(vercmp "$_installed_version" "$_latest_version")" + if [[ "$_vercmp" -lt 0 ]]; then + return 0 + fi + + return 1 +} + + +### +# ENTRY +### + +perform_install_hysteria_binary() { + if [[ -n "$LOCAL_FILE" ]]; then + note "Performing local install: $LOCAL_FILE" + + echo -ne "Installing hysteria executable ... " + + if install -Dm755 "$LOCAL_FILE" "$EXECUTABLE_INSTALL_PATH"; then + echo "ok" + else + exit 2 + fi + + return + fi + + local _tmpfile=$(mktemp) + + if ! download_hysteria "$VERSION" "$_tmpfile"; then + rm -f "$_tmpfile" + exit 11 + fi + + echo -ne "Installing hysteria executable ... " + + if install -Dm755 "$_tmpfile" "$EXECUTABLE_INSTALL_PATH"; then + echo "ok" + else + exit 13 + fi + + rm -f "$_tmpfile" +} + +perform_remove_hysteria_binary() { + remove_file "$EXECUTABLE_INSTALL_PATH" +} + +perform_install_hysteria_example_config() { + install_content -Dm644 "$(tpl_etc_hysteria_config_yaml)" "$CONFIG_DIR/config.yaml" "" +} + +perform_install_hysteria_systemd() { + if [[ "x$FORCE_NO_SYSTEMD" == "x2" ]]; then + return + fi + + install_content -Dm644 "$(tpl_hysteria_server_service)" "$SYSTEMD_SERVICES_DIR/hysteria-server.service" "1" + install_content -Dm644 "$(tpl_hysteria_server_x_service)" "$SYSTEMD_SERVICES_DIR/hysteria-server@.service" "1" + if [[ -n "$SECONTEXT_SYSTEMD_UNIT" ]]; then + chcon "$SECONTEXT_SYSTEMD_UNIT" "$SYSTEMD_SERVICES_DIR/hysteria-server.service" + chcon "$SECONTEXT_SYSTEMD_UNIT" "$SYSTEMD_SERVICES_DIR/hysteria-server@.service" + fi + + systemctl daemon-reload +} + +perform_remove_hysteria_systemd() { + remove_file "$SYSTEMD_SERVICES_DIR/hysteria-server.service" + remove_file "$SYSTEMD_SERVICES_DIR/hysteria-server@.service" + + systemctl daemon-reload +} + +perform_install_hysteria_home_legacy() { + if ! is_user_exists "$HYSTERIA_USER"; then + echo -ne "Creating user $HYSTERIA_USER ... " + useradd -r -d "$HYSTERIA_HOME_DIR" -m "$HYSTERIA_USER" + echo "ok" + fi +} + +perform_install() { + local _is_frash_install + local _is_upgrade_from_hysteria1 + if ! is_hysteria_installed; then + _is_frash_install=1 + elif is_hysteria1_version "$(get_installed_version)"; then + _is_upgrade_from_hysteria1=1 + fi + + local _is_update_required + + if [[ -n "$LOCAL_FILE" ]] || [[ -n "$VERSION" ]] || check_update; then + _is_update_required=1 + fi + + if [[ "x$FORCE" == "x1" ]]; then + if [[ -z "$_is_update_required" ]]; then + note "Option '--force' detected, re-install even if installed version is the latest." + fi + _is_update_required=1 + fi + + if is_hysteria1_version "$VERSION"; then + error "This script can only install Hysteria 2." + exit 95 + fi + + if [[ -n "$_is_update_required" ]]; then + perform_install_hysteria_binary + fi + + # Always install additional files, regardless of $_is_update_required. + # This allows changes to be made with environment variables (e.g. change HYSTERIA_USER without --force). + perform_install_hysteria_example_config + perform_install_hysteria_home_legacy + perform_install_hysteria_systemd + + if [[ -z "$_is_update_required" ]]; then + echo + echo "$(tgreen)Installed version is up-to-date, there is nothing to do.$(treset)" + echo + elif [[ -n "$_is_frash_install" ]]; then + echo + echo -e "$(tbold)Congratulation! Hysteria 2 has been successfully installed on your server.$(treset)" + echo + echo -e "What's next?" + echo + echo -e "\t+ Take a look at the differences between Hysteria 2 and Hysteria 1 at https://hysteria.network/docs/misc/2-vs-1/" + echo -e "\t+ Check out the quick server config guide at $(tblue)https://hysteria.network/docs/getting-started/Server/$(treset)" + echo -e "\t+ Edit server config file at $(tred)$CONFIG_DIR/config.yaml$(treset)" + echo -e "\t+ Start your hysteria server with $(tred)systemctl start hysteria-server.service$(treset)" + echo -e "\t+ Configure hysteria start on system boot with $(tred)systemctl enable hysteria-server.service$(treset)" + echo + elif [[ -n "$_is_upgrade_from_hysteria1" ]]; then + echo -e "Skip automatic service restart due to $(tred)incompatible$(treset) upgrade." + echo + echo -e "$(tbold)Hysteria has been successfully update to $VERSION from Hysteria 1.$(treset)" + echo + echo -e "$(tred)Hysteria 2 uses a completely redesigned protocol & config, which is NOT compatible with the version 1.x.x in any way.$(treset)" + echo + echo -e "\t+ Take a look at the behavior changes in Hysteria 2 at $(tblue)https://hysteria.network/docs/misc/2-vs-1/$(treset)" + echo -e "\t+ Check out the quick server configuration guide for Hysteria 2 at $(tblue)https://hysteria.network/docs/getting-started/Server/$(treset)" + echo -e "\t+ Migrate server config file to the Hysteria 2 at $(tred)$CONFIG_DIR/config.yaml$(treset)" + echo -e "\t+ Start your hysteria server with $(tred)systemctl restart hysteria-server.service$(treset)" + echo -e "\t+ Configure hysteria start on system boot with $(tred)systemctl enable hysteria-server.service$(treset)" + else + restart_running_services + + echo + echo -e "$(tbold)Hysteria has been successfully update to $VERSION.$(treset)" + echo + echo -e "Check out the latest changelog $(tblue)https://github.com/apernet/hysteria/blob/master/CHANGELOG.md$(treset)" + echo + fi +} + +perform_remove() { + perform_remove_hysteria_binary + stop_running_services + perform_remove_hysteria_systemd + + echo + echo -e "$(tbold)Congratulation! Hysteria has been successfully removed from your server.$(treset)" + echo + echo -e "You still need to remove configuration files and ACME certificates manually with the following commands:" + echo + echo -e "\t$(tred)rm -rf "$CONFIG_DIR"$(treset)" + if [[ "x$HYSTERIA_USER" != "xroot" ]]; then + echo -e "\t$(tred)userdel -r "$HYSTERIA_USER"$(treset)" + fi + if [[ "x$FORCE_NO_SYSTEMD" != "x2" ]]; then + echo + echo -e "You still might need to disable all related systemd services with the following commands:" + echo + echo -e "\t$(tred)rm -f /etc/systemd/system/multi-user.target.wants/hysteria-server.service$(treset)" + echo -e "\t$(tred)rm -f /etc/systemd/system/multi-user.target.wants/hysteria-server@*.service$(treset)" + echo -e "\t$(tred)systemctl daemon-reload$(treset)" + fi + echo +} + +perform_check_update() { + if check_update; then + echo + echo -e "$(tbold)Update available: $VERSION$(treset)" + echo + echo -e "$(tgreen)You can download and install the latest version by execute this script without any arguments.$(treset)" + echo + else + echo + echo "$(tgreen)Installed version is up-to-date.$(treset)" + echo + fi +} + +main() { + parse_arguments "$@" + + check_permission + check_environment + check_hysteria_user "hysteria" + check_hysteria_homedir "/var/lib/$HYSTERIA_USER" + + case "$OPERATION" in + "install") + perform_install + ;; + "remove") + perform_remove + ;; + "check_update") + perform_check_update + ;; + *) + error "Unknown operation '$OPERATION'." + ;; + esac +} + +main "$@" + +# vim:set ft=bash ts=2 sw=2 sts=2 et: diff --git a/v2rayng/libhysteria2.sh b/v2rayng/libhysteria2.sh new file mode 100644 index 0000000000..5c0d6fd02a --- /dev/null +++ b/v2rayng/libhysteria2.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +targets=( + "aarch64-linux-android21 arm64 arm64-v8a" + "armv7a-linux-androideabi21 arm armeabi-v7a" + "x86_64-linux-android21 amd64 x86_64" + "i686-linux-android21 386 x86" +) + +cd "hysteria" || exit + +for target in "${targets[@]}"; do + IFS=' ' read -r ndk_target goarch abi <<< "$target" + + echo "Building for ${abi} with ${ndk_target} (${goarch})" + + CC="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/${ndk_target}-clang" CGO_ENABLED=1 CGO_LDFLAGS="-Wl,-z,max-page-size=16384" GOOS=android GOARCH=$goarch go build -o libs/$abi/libhysteria2.so -trimpath -ldflags "-s -w -buildid=" ./app + + echo "Built libhysteria2.so for ${abi}" +done