mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-04-13 05:20:28 +08:00
feat(tun): validate route-exclude-address as CIDR (#6440)
* feat(tun): validate route-exclude-address as CIDR * refactor(network): replace ipaddr.js helpers with cidr-block and validator * docs: Changelog
This commit is contained in:
parent
c429632d80
commit
0bbf9407d8
@ -22,5 +22,6 @@
|
||||
- 优化托盘退出快捷键显示
|
||||
- 优化首次启动节点信息刷新
|
||||
- Linux 默认使用内置窗口控件
|
||||
- 实现排除自定义网段的校验
|
||||
|
||||
</details>
|
||||
|
||||
@ -56,11 +56,11 @@
|
||||
"@tauri-apps/plugin-updater": "2.10.0",
|
||||
"ahooks": "^3.9.6",
|
||||
"axios": "^1.13.6",
|
||||
"cidr-block": "^2.3.0",
|
||||
"dayjs": "1.11.19",
|
||||
"foxact": "^0.2.54",
|
||||
"foxts": "^5.3.0",
|
||||
"i18next": "^25.8.13",
|
||||
"ipaddr.js": "^2.3.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"lodash-es": "^4.17.23",
|
||||
"monaco-editor": "^0.55.1",
|
||||
@ -77,7 +77,8 @@
|
||||
"rehype-raw": "^7.0.0",
|
||||
"swr": "^2.4.1",
|
||||
"tauri-plugin-mihomo-api": "github:clash-verge-rev/tauri-plugin-mihomo#main",
|
||||
"types-pac": "^1.0.3"
|
||||
"types-pac": "^1.0.3",
|
||||
"validator": "^13.15.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^9.0.0",
|
||||
@ -89,6 +90,7 @@
|
||||
"@types/node": "^24.11.0",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/validator": "^13.15.10",
|
||||
"@vitejs/plugin-legacy": "^7.2.1",
|
||||
"@vitejs/plugin-react-swc": "^4.2.3",
|
||||
"adm-zip": "^0.5.16",
|
||||
|
||||
35
pnpm-lock.yaml
generated
35
pnpm-lock.yaml
generated
@ -74,6 +74,9 @@ importers:
|
||||
axios:
|
||||
specifier: ^1.13.6
|
||||
version: 1.13.6
|
||||
cidr-block:
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
dayjs:
|
||||
specifier: 1.11.19
|
||||
version: 1.11.19
|
||||
@ -86,9 +89,6 @@ importers:
|
||||
i18next:
|
||||
specifier: ^25.8.13
|
||||
version: 25.8.13(typescript@5.9.3)
|
||||
ipaddr.js:
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
js-yaml:
|
||||
specifier: ^4.1.1
|
||||
version: 4.1.1
|
||||
@ -140,6 +140,9 @@ importers:
|
||||
types-pac:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
validator:
|
||||
specifier: ^13.15.26
|
||||
version: 13.15.26
|
||||
devDependencies:
|
||||
'@actions/github':
|
||||
specifier: ^9.0.0
|
||||
@ -168,6 +171,9 @@ importers:
|
||||
'@types/react-dom':
|
||||
specifier: 19.2.3
|
||||
version: 19.2.3(@types/react@19.2.14)
|
||||
'@types/validator':
|
||||
specifier: ^13.15.10
|
||||
version: 13.15.10
|
||||
'@vitejs/plugin-legacy':
|
||||
specifier: ^7.2.1
|
||||
version: 7.2.1(terser@5.46.0)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))
|
||||
@ -1882,6 +1888,9 @@ packages:
|
||||
'@types/unist@3.0.3':
|
||||
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||
|
||||
'@types/validator@13.15.10':
|
||||
resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.56.1':
|
||||
resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@ -2211,6 +2220,10 @@ packages:
|
||||
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
cidr-block@2.3.0:
|
||||
resolution: {integrity: sha512-U5NE6QJKNOhIsVrI6sicbwGtEmEORbrz9B5Mbl+Mi05sz1Ic4egoLGV4DkJp56VWva61WxOf+vzIxTKEmRazqg==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
cli-color@2.0.4:
|
||||
resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==}
|
||||
engines: {node: '>=0.10'}
|
||||
@ -2821,10 +2834,6 @@ packages:
|
||||
resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==}
|
||||
deprecated: The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019.
|
||||
|
||||
ipaddr.js@2.3.0:
|
||||
resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
is-alphabetical@2.0.1:
|
||||
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
|
||||
|
||||
@ -3672,6 +3681,10 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
validator@13.15.26:
|
||||
resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
vfile-location@5.0.3:
|
||||
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
|
||||
|
||||
@ -5443,6 +5456,8 @@ snapshots:
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
|
||||
'@types/validator@13.15.10': {}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
@ -5772,6 +5787,8 @@ snapshots:
|
||||
|
||||
chownr@3.0.0: {}
|
||||
|
||||
cidr-block@2.3.0: {}
|
||||
|
||||
cli-color@2.0.4:
|
||||
dependencies:
|
||||
d: 1.0.2
|
||||
@ -6512,8 +6529,6 @@ snapshots:
|
||||
|
||||
intersection-observer@0.12.2: {}
|
||||
|
||||
ipaddr.js@2.3.0: {}
|
||||
|
||||
is-alphabetical@2.0.1: {}
|
||||
|
||||
is-alphanumerical@2.0.1:
|
||||
@ -7526,6 +7541,8 @@ snapshots:
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
|
||||
validator@13.15.26: {}
|
||||
|
||||
vfile-location@5.0.3:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
@ -49,6 +49,7 @@ import { showNotice } from "@/services/notice-service";
|
||||
import { useThemeMode } from "@/services/states";
|
||||
import type { TranslationKey } from "@/types/generated/i18n-keys";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import { isValidIpCidr } from "@/utils/network";
|
||||
|
||||
interface Props {
|
||||
groupsUid: string;
|
||||
@ -65,16 +66,6 @@ const portValidator = (value: string): boolean => {
|
||||
"^(?:[1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$",
|
||||
).test(value);
|
||||
};
|
||||
const ipv4CIDRValidator = (value: string): boolean => {
|
||||
return new RegExp(
|
||||
"^(?:(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))\\.){3}(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))(?:\\/(?:[12]?[0-9]|3[0-2]))$",
|
||||
).test(value);
|
||||
};
|
||||
const ipv6CIDRValidator = (value: string): boolean => {
|
||||
return new RegExp(
|
||||
"^([0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){7}|::|:(?::[0-9a-fA-F]{1,4}){1,6}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){5}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:)\\/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])$",
|
||||
).test(value);
|
||||
};
|
||||
|
||||
const rules: {
|
||||
name: string;
|
||||
@ -127,29 +118,29 @@ const rules: {
|
||||
name: "IP-CIDR",
|
||||
example: "127.0.0.0/8",
|
||||
noResolve: true,
|
||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||
validator: isValidIpCidr,
|
||||
},
|
||||
{
|
||||
name: "IP-CIDR6",
|
||||
example: "2620:0:2d0:200::7/32",
|
||||
noResolve: true,
|
||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||
validator: isValidIpCidr,
|
||||
},
|
||||
{
|
||||
name: "SRC-IP-CIDR",
|
||||
example: "192.168.1.201/32",
|
||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||
validator: isValidIpCidr,
|
||||
},
|
||||
{
|
||||
name: "IP-SUFFIX",
|
||||
example: "8.8.8.8/24",
|
||||
noResolve: true,
|
||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||
validator: isValidIpCidr,
|
||||
},
|
||||
{
|
||||
name: "SRC-IP-SUFFIX",
|
||||
example: "192.168.1.201/8",
|
||||
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
|
||||
validator: isValidIpCidr,
|
||||
},
|
||||
{
|
||||
name: "SRC-PORT",
|
||||
|
||||
@ -21,7 +21,7 @@ import {
|
||||
getWebdavStatus,
|
||||
setWebdavStatus,
|
||||
} from "@/services/webdav-status";
|
||||
import { isValidUrl } from "@/utils/helper";
|
||||
import { isValidUrl } from "@/utils/network";
|
||||
|
||||
interface BackupConfigViewerProps {
|
||||
onBackupSuccess: () => Promise<void>;
|
||||
|
||||
@ -23,6 +23,7 @@ import { useClash } from "@/hooks/use-clash";
|
||||
import { enhanceProfiles } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/notice-service";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import { areValidIpCidrs } from "@/utils/network";
|
||||
|
||||
import { StackModeSwitch } from "./stack-mode-switch";
|
||||
|
||||
@ -52,6 +53,17 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
mtu: 1500,
|
||||
});
|
||||
|
||||
const routeExcludeAddressItems = splitRouteExcludeAddress(
|
||||
values.routeExcludeAddress,
|
||||
);
|
||||
const routeExcludeAddressError =
|
||||
values.autoRoute &&
|
||||
routeExcludeAddressItems.length > 0 &&
|
||||
!areValidIpCidrs(routeExcludeAddressItems);
|
||||
const routeExcludeAddressHelperText = routeExcludeAddressError
|
||||
? t("settings.modals.tun.messages.invalidRouteExcludeAddress")
|
||||
: t("settings.modals.tun.messages.routeExcludeAddressHint");
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => {
|
||||
setOpen(true);
|
||||
@ -78,9 +90,15 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
|
||||
const onSave = useLockFn(async () => {
|
||||
try {
|
||||
const routeExcludeAddress = splitRouteExcludeAddress(
|
||||
values.routeExcludeAddress,
|
||||
);
|
||||
const routeExcludeAddress = routeExcludeAddressItems;
|
||||
|
||||
if (routeExcludeAddressError) {
|
||||
showNotice.error(
|
||||
"settings.modals.tun.messages.invalidRouteExcludeAddress",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const tun: IConfigData["tun"] = {
|
||||
stack: values.stack,
|
||||
device:
|
||||
@ -312,6 +330,8 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
placeholder="192.168.0.0/16"
|
||||
ariaLabel={t("settings.modals.tun.fields.routeExcludeAddress")}
|
||||
disabled={!values.autoRoute}
|
||||
error={routeExcludeAddressError}
|
||||
helperText={routeExcludeAddressHelperText}
|
||||
onChange={(nextValue) =>
|
||||
setValues((v) => ({ ...v, routeExcludeAddress: nextValue }))
|
||||
}
|
||||
|
||||
@ -19,7 +19,12 @@ import { useClash } from "@/hooks/use-clash";
|
||||
import { useAppData } from "@/providers/app-data-context";
|
||||
import { isPortInUse } from "@/services/cmds";
|
||||
import { showNotice } from "@/services/notice-service";
|
||||
import { parseHost, parsedLocalhost, isValidPort } from "@/utils/helper";
|
||||
import {
|
||||
formatHostPort,
|
||||
isValidPort,
|
||||
normalizeHost,
|
||||
normalizeListenHost,
|
||||
} from "@/utils/network";
|
||||
|
||||
interface TunnelsViewerRef {
|
||||
open: () => void;
|
||||
@ -127,8 +132,8 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
|
||||
}
|
||||
|
||||
// 本地地址校验(host)
|
||||
const parsedLocal = parsedLocalhost(localAddr);
|
||||
if (!parsedLocal) {
|
||||
const localHost = normalizeListenHost(localAddr);
|
||||
if (!localHost) {
|
||||
showNotice.error(
|
||||
"settings.sections.clash.form.fields.tunnels.messages.invalidLocalAddr",
|
||||
);
|
||||
@ -151,8 +156,8 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
|
||||
}
|
||||
|
||||
// 目标地址校验 (host)
|
||||
const parsedTarget = parseHost(targetAddr);
|
||||
if (!parsedTarget) {
|
||||
const targetHost = normalizeHost(targetAddr);
|
||||
if (!targetHost) {
|
||||
showNotice.error(
|
||||
"settings.sections.clash.form.fields.tunnels.messages.invalidTargetAddr",
|
||||
);
|
||||
@ -170,14 +175,8 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
|
||||
// 构造新 entry
|
||||
const entry: TunnelEntry = {
|
||||
network: network === "tcp+udp" ? ["tcp", "udp"] : [network],
|
||||
address:
|
||||
parsedLocal.kind === "ipv6"
|
||||
? `[${parsedLocal.host}]:${localPort}`
|
||||
: `${parsedLocal.host}:${localPort}`,
|
||||
target:
|
||||
parsedTarget.kind === "ipv6"
|
||||
? `[${parsedTarget.host}]:${targetPort}`
|
||||
: `${parsedTarget.host}:${targetPort}`,
|
||||
address: formatHostPort(localHost, localPort),
|
||||
target: formatHostPort(targetHost, targetPort),
|
||||
...(proxy ? { proxy } : {}),
|
||||
};
|
||||
|
||||
|
||||
@ -481,7 +481,9 @@
|
||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||
},
|
||||
"messages": {
|
||||
"applied": "تم تطبيق الإعدادات"
|
||||
"applied": "تم تطبيق الإعدادات",
|
||||
"invalidRouteExcludeAddress": "يرجى إدخال نطاق CIDR صالح",
|
||||
"routeExcludeAddressHint": "يتم دعم CIDR لـ IPv4/IPv6 فقط، مثل 192.168.0.0/16 أو fd00::/8"
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
|
||||
@ -481,7 +481,9 @@
|
||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||
},
|
||||
"messages": {
|
||||
"applied": "Einstellungen angewendet"
|
||||
"applied": "Einstellungen angewendet",
|
||||
"invalidRouteExcludeAddress": "Bitte geben Sie einen gültigen CIDR-Block ein",
|
||||
"routeExcludeAddressHint": "Es werden nur IPv4-/IPv6-CIDR-Blöcke unterstützt, z. B. 192.168.0.0/16 oder fd00::/8"
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
|
||||
@ -481,7 +481,9 @@
|
||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||
},
|
||||
"messages": {
|
||||
"applied": "Settings Applied"
|
||||
"applied": "Settings Applied",
|
||||
"invalidRouteExcludeAddress": "Please enter a valid CIDR block",
|
||||
"routeExcludeAddressHint": "Only IPv4/IPv6 CIDR is supported, such as 192.168.0.0/16 or fd00::/8"
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
|
||||
@ -481,7 +481,9 @@
|
||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||
},
|
||||
"messages": {
|
||||
"applied": "Ajustes aplicados"
|
||||
"applied": "Ajustes aplicados",
|
||||
"invalidRouteExcludeAddress": "Introduce un bloque CIDR válido",
|
||||
"routeExcludeAddressHint": "Solo se admiten bloques CIDR de IPv4/IPv6, como 192.168.0.0/16 o fd00::/8"
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
|
||||
@ -481,7 +481,9 @@
|
||||
"autoRedirect": "پیکربندی خودکار ریدایرکتهای TCP در nftables/iptables"
|
||||
},
|
||||
"messages": {
|
||||
"applied": "تنظیمات اعمال شد"
|
||||
"applied": "تنظیمات اعمال شد",
|
||||
"invalidRouteExcludeAddress": "لطفاً یک محدوده CIDR معتبر وارد کنید",
|
||||
"routeExcludeAddressHint": "فقط CIDRهای IPv4/IPv6 پشتیبانی میشوند، مانند 192.168.0.0/16 یا fd00::/8"
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
|
||||
@ -481,7 +481,9 @@
|
||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||
},
|
||||
"messages": {
|
||||
"applied": "Pengaturan Diterapkan"
|
||||
"applied": "Pengaturan Diterapkan",
|
||||
"invalidRouteExcludeAddress": "Masukkan blok CIDR yang valid",
|
||||
"routeExcludeAddressHint": "Hanya CIDR IPv4/IPv6 yang didukung, seperti 192.168.0.0/16 atau fd00::/8"
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
|
||||
@ -481,7 +481,9 @@
|
||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||
},
|
||||
"messages": {
|
||||
"applied": "設定が適用されました。"
|
||||
"applied": "設定が適用されました",
|
||||
"invalidRouteExcludeAddress": "有効な CIDR ブロックを入力してください",
|
||||
"routeExcludeAddressHint": "IPv4/IPv6 の CIDR のみ対応しています。例: 192.168.0.0/16、fd00::/8"
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
|
||||
@ -481,7 +481,9 @@
|
||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||
},
|
||||
"messages": {
|
||||
"applied": "설정이 적용되었습니다"
|
||||
"applied": "설정이 적용되었습니다",
|
||||
"invalidRouteExcludeAddress": "유효한 CIDR 대역을 입력하세요",
|
||||
"routeExcludeAddressHint": "IPv4/IPv6 CIDR만 지원합니다. 예: 192.168.0.0/16 또는 fd00::/8"
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
|
||||
@ -481,7 +481,9 @@
|
||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||
},
|
||||
"messages": {
|
||||
"applied": "Настройки применены"
|
||||
"applied": "Настройки применены",
|
||||
"invalidRouteExcludeAddress": "Введите корректный блок CIDR",
|
||||
"routeExcludeAddressHint": "Поддерживаются только блоки CIDR IPv4/IPv6, например 192.168.0.0/16 или fd00::/8"
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
|
||||
@ -481,7 +481,9 @@
|
||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||
},
|
||||
"messages": {
|
||||
"applied": "Ayarlar Uygulandı"
|
||||
"applied": "Ayarlar Uygulandı",
|
||||
"invalidRouteExcludeAddress": "Lütfen geçerli bir CIDR bloğu girin",
|
||||
"routeExcludeAddressHint": "Yalnızca IPv4/IPv6 CIDR blokları desteklenir; örneğin 192.168.0.0/16 veya fd00::/8"
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
|
||||
@ -481,7 +481,9 @@
|
||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||
},
|
||||
"messages": {
|
||||
"applied": "Көйләүләр кулланылды"
|
||||
"applied": "Көйләүләр кулланылды",
|
||||
"invalidRouteExcludeAddress": "Дөрес CIDR блогын кертегез",
|
||||
"routeExcludeAddressHint": "IPv4/IPv6 CIDR блоклары гына хуплана, мәсәлән 192.168.0.0/16 яки fd00::/8"
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
|
||||
@ -481,7 +481,9 @@
|
||||
"autoRedirect": "自动配置 nftables/iptables 的 TCP 重定向"
|
||||
},
|
||||
"messages": {
|
||||
"applied": "设置已应用"
|
||||
"applied": "设置已应用",
|
||||
"invalidRouteExcludeAddress": "请输入有效的 CIDR 网段",
|
||||
"routeExcludeAddressHint": "仅支持 IPv4/IPv6 CIDR,例如 192.168.0.0/16 或 fd00::/8"
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
|
||||
@ -481,7 +481,9 @@
|
||||
"autoRedirect": "自動配置 nftables/iptables 的 TCP 重導"
|
||||
},
|
||||
"messages": {
|
||||
"applied": "設定已套用"
|
||||
"applied": "設定已套用",
|
||||
"invalidRouteExcludeAddress": "請輸入有效的 CIDR 網段",
|
||||
"routeExcludeAddressHint": "僅支援 IPv4/IPv6 CIDR,例如 192.168.0.0/16 或 fd00::/8"
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
|
||||
@ -602,6 +602,8 @@ export const translationKeys = [
|
||||
"settings.modals.tun.tooltips.dnsHijack",
|
||||
"settings.modals.tun.tooltips.autoRedirect",
|
||||
"settings.modals.tun.messages.applied",
|
||||
"settings.modals.tun.messages.invalidRouteExcludeAddress",
|
||||
"settings.modals.tun.messages.routeExcludeAddressHint",
|
||||
"settings.modals.dns.dialog.title",
|
||||
"settings.modals.dns.dialog.warning",
|
||||
"settings.modals.dns.sections.general",
|
||||
|
||||
@ -1024,6 +1024,8 @@ export interface TranslationResources {
|
||||
};
|
||||
messages: {
|
||||
applied: string;
|
||||
invalidRouteExcludeAddress: string;
|
||||
routeExcludeAddressHint: string;
|
||||
};
|
||||
title: string;
|
||||
tooltips: {
|
||||
@ -1318,10 +1320,10 @@ export interface TranslationResources {
|
||||
usedTotal: string;
|
||||
};
|
||||
placeholders: {
|
||||
resetInput: string;
|
||||
filter: string;
|
||||
matchCase: string;
|
||||
matchWholeWord: string;
|
||||
resetInput: string;
|
||||
useRegex: string;
|
||||
};
|
||||
statuses: {
|
||||
|
||||
@ -1,112 +0,0 @@
|
||||
import ipaddr from "ipaddr.js";
|
||||
|
||||
import { debugLog } from "@/utils/debug";
|
||||
|
||||
export type HostKind = "localhost" | "domain" | "ipv4" | "ipv6";
|
||||
|
||||
export type ParsedHost = {
|
||||
kind: HostKind;
|
||||
host: string;
|
||||
};
|
||||
|
||||
export const isValidUrl = (url: string) => {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugLog(e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const isValidPort = (port: string) => {
|
||||
const portNumber = Number(port);
|
||||
return Number.isInteger(portNumber) && portNumber > 0 && portNumber < 65536;
|
||||
};
|
||||
|
||||
export const isValidDomain = (domain: string) => {
|
||||
try {
|
||||
const url = new URL(`http://${domain}`);
|
||||
return url.hostname.toLowerCase() === domain.toLowerCase();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const isLocalhostString = (host: string) => {
|
||||
const lowerHost = host.toLowerCase();
|
||||
return lowerHost === "localhost";
|
||||
};
|
||||
|
||||
const stripBrackets = (host: string): string =>
|
||||
host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
||||
|
||||
export const parseHost = (host: string): ParsedHost | null => {
|
||||
// 去除前后空白
|
||||
host = host.trim();
|
||||
// 优先检测 localhost
|
||||
if (isLocalhostString(host)) {
|
||||
return { kind: "localhost", host: "localhost" };
|
||||
}
|
||||
// 除去方括号后检测 IP
|
||||
const strippedHost = stripBrackets(host);
|
||||
// IPv4
|
||||
if (ipaddr.IPv4.isValidFourPartDecimal(strippedHost)) {
|
||||
return { kind: "ipv4", host: strippedHost };
|
||||
}
|
||||
// IPv6
|
||||
if (ipaddr.IPv6.isValid(strippedHost)) {
|
||||
return { kind: "ipv6", host: strippedHost };
|
||||
}
|
||||
// 再检测 domain, 防止像 [::1] 这样的合法 IPv6 地址被误判为域名
|
||||
if (isValidDomain(host)) {
|
||||
return { kind: "domain", host };
|
||||
}
|
||||
// 都不是
|
||||
return null;
|
||||
};
|
||||
|
||||
const LISTEN_CIDRS_V4 = [
|
||||
"0.0.0.0/32", // any
|
||||
"127.0.0.0/8", // loopback
|
||||
"10.0.0.0/8", // RFC1918
|
||||
"172.16.0.0/12", // RFC1918
|
||||
"192.168.0.0/16", // RFC1918
|
||||
];
|
||||
|
||||
const LISTEN_CIDRS_V6 = [
|
||||
"::/128", // any
|
||||
"::1/128", // loopback
|
||||
"fc00::/7", // ULA(LAN)
|
||||
"fe80::/10", // link-local
|
||||
];
|
||||
|
||||
export const parsedLocalhost = (host: string): ParsedHost | null => {
|
||||
// 先 parse 一下
|
||||
const parsed = parseHost(host);
|
||||
if (!parsed) return null;
|
||||
// localhost 直接通过
|
||||
if (parsed.kind === "localhost") {
|
||||
return parsed;
|
||||
}
|
||||
// IP 则检查是否在允许的 CIDR 列表内
|
||||
if (parsed.kind === "ipv4") {
|
||||
// IPv4
|
||||
const addr = ipaddr.IPv4.parse(parsed.host);
|
||||
for (const cidr of LISTEN_CIDRS_V4) {
|
||||
if (addr.match(ipaddr.IPv4.parseCIDR(cidr))) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
} else if (parsed.kind === "ipv6") {
|
||||
// IPv6
|
||||
const addr = ipaddr.IPv6.parse(parsed.host);
|
||||
for (const cidr of LISTEN_CIDRS_V6) {
|
||||
if (addr.match(ipaddr.IPv6.parseCIDR(cidr))) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
// domain和都不符合则返回 null
|
||||
return null;
|
||||
};
|
||||
87
src/utils/network.ts
Normal file
87
src/utils/network.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { ipv4, ipv6 } from "cidr-block";
|
||||
import validator from "validator";
|
||||
|
||||
const stripBrackets = (value: string) =>
|
||||
value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value;
|
||||
|
||||
const isIpv4 = (value: string) => validator.isIP(value, 4);
|
||||
const isIpv6 = (value: string) => validator.isIP(value, 6);
|
||||
const isHostname = (value: string) =>
|
||||
validator.isFQDN(value, { require_tld: false });
|
||||
|
||||
export const isValidUrl = (value: string) =>
|
||||
validator.isURL(value.trim(), {
|
||||
protocols: ["http", "https"],
|
||||
require_protocol: true,
|
||||
require_valid_protocol: true,
|
||||
require_host: true,
|
||||
require_tld: false,
|
||||
});
|
||||
|
||||
export const isValidPort = (value: string) => validator.isPort(value.trim());
|
||||
|
||||
export const normalizeHost = (value: string): string | null => {
|
||||
const host = value.trim();
|
||||
if (!host) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (host.toLowerCase() === "localhost") {
|
||||
return "localhost";
|
||||
}
|
||||
|
||||
const normalizedHost = stripBrackets(host);
|
||||
if (isIpv4(normalizedHost) || isIpv6(normalizedHost)) {
|
||||
return normalizedHost;
|
||||
}
|
||||
|
||||
return isHostname(host) ? host : null;
|
||||
};
|
||||
|
||||
export const normalizeListenHost = (value: string): string | null => {
|
||||
const host = normalizeHost(value);
|
||||
if (!host || host === "localhost") {
|
||||
return host;
|
||||
}
|
||||
|
||||
if (isIpv4(host)) {
|
||||
const address = ipv4.address(host);
|
||||
return address.equals("0.0.0.0") ||
|
||||
address.isLoopbackAddress() ||
|
||||
address.isPrivateAddress()
|
||||
? host
|
||||
: null;
|
||||
}
|
||||
|
||||
if (!isIpv6(host)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const address = ipv6.address(host);
|
||||
return address.isUnspecifiedAddress() ||
|
||||
address.isLoopbackAddress() ||
|
||||
address.isUniqueLocalAddress() ||
|
||||
address.isLinkLocalAddress()
|
||||
? host
|
||||
: null;
|
||||
};
|
||||
|
||||
export const formatHostPort = (host: string, port: string | number) =>
|
||||
isIpv6(host) ? `[${host}]:${port}` : `${host}:${port}`;
|
||||
|
||||
export const isValidIpCidr = (value: string): boolean => {
|
||||
const cidr = value.trim();
|
||||
const slashIndex = cidr.lastIndexOf("/");
|
||||
if (slashIndex <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const address = cidr.slice(0, slashIndex);
|
||||
return (
|
||||
(isIpv4(address) && ipv4.isValidCIDR(cidr)) ||
|
||||
(isIpv6(address) && ipv6.isValidCIDR(cidr))
|
||||
);
|
||||
};
|
||||
|
||||
export const areValidIpCidrs = (values: string[]) =>
|
||||
values.every(isValidIpCidr);
|
||||
Loading…
x
Reference in New Issue
Block a user