diff --git a/vite-frontend/src/pages/node/tunnel-monitor-view.tsx b/vite-frontend/src/pages/node/tunnel-monitor-view.tsx
index 3eec8a3..b51aeeb 100644
--- a/vite-frontend/src/pages/node/tunnel-monitor-view.tsx
+++ b/vite-frontend/src/pages/node/tunnel-monitor-view.tsx
@@ -1170,6 +1170,7 @@ export function TunnelMonitorView({
{tunnels.map((tunnel) => {
const quality = qualityMap[tunnel.id];
const isEnabled = tunnel.status === 1;
+ const targetLabel = probeTargetLabel(quality);
return (
-
+
出口→测试目标
+
+ {targetLabel}
+
{
const quality = qualityMap[tunnel.id];
const isEnabled = tunnel.status === 1;
+ const targetLabel = probeTargetLabel(quality);
return (
-
-
+
+
+
+
+ {targetLabel}
+
+
{quality?.timestamp ? (
diff --git a/vite-frontend/src/pages/tunnel/form.ts b/vite-frontend/src/pages/tunnel/form.ts
index bb7b4f1..e6b636c 100644
--- a/vite-frontend/src/pages/tunnel/form.ts
+++ b/vite-frontend/src/pages/tunnel/form.ts
@@ -17,6 +17,93 @@ interface TunnelNodeInput {
status: number;
}
+const isValidProbeIPv4 = (host: string) => {
+ const parts = host.split(".");
+
+ return (
+ parts.length === 4 &&
+ parts.every((part) => {
+ if (!/^\d+$/.test(part)) {
+ return false;
+ }
+
+ const value = Number(part);
+
+ return value >= 0 && value <= 255;
+ })
+ );
+};
+
+const isIPv4LikeProbeHost = (host: string) =>
+ /^[0-9.]+$/.test(host) && host.includes(".");
+
+const isValidProbeIPv6 = (host: string) => {
+ let value = host;
+
+ if (host.startsWith("[") || host.endsWith("]")) {
+ if (!host.startsWith("[") || !host.endsWith("]")) {
+ return false;
+ }
+ value = host.slice(1, -1);
+ }
+
+ if (!value.includes(":") || value.includes("[") || value.includes("]")) {
+ return false;
+ }
+
+ try {
+ const url = new URL(`http://[${value}]`);
+
+ return url.hostname.length > 0;
+ } catch {
+ return false;
+ }
+};
+
+const isSchemeLikeProbeHost = (host: string) => {
+ if (isValidProbeIPv6(host)) {
+ return false;
+ }
+
+ const colonIndex = host.indexOf(":");
+
+ if (colonIndex <= 0) {
+ return false;
+ }
+
+ return /^[A-Za-z][A-Za-z0-9+.-]*$/.test(host.slice(0, colonIndex));
+};
+
+const isValidProbeDomain = (host: string) => {
+ if (!host || host.length > 253) {
+ return false;
+ }
+
+ return host.split(".").every((label) => {
+ if (
+ !label ||
+ label.length > 63 ||
+ label.startsWith("-") ||
+ label.endsWith("-")
+ ) {
+ return false;
+ }
+
+ return /^[A-Za-z0-9-]+$/.test(label);
+ });
+};
+
+const isValidProbeTargetHost = (host: string) => {
+ if (isValidProbeIPv6(host) || isValidProbeIPv4(host)) {
+ return true;
+ }
+ if (host.includes(":") || isIPv4LikeProbeHost(host)) {
+ return false;
+ }
+
+ return isValidProbeDomain(host);
+};
+
export const createTunnelFormDefaults = () => {
return {
name: "",
@@ -75,8 +162,14 @@ export const validateTunnelForm = (
if (probeHost || hasProbePort) {
if (!probeHost) {
errors.probeTargetHost = "请输入测试目标 Host";
- } else if (probeHost.includes("://") || /[\s/?#]/.test(probeHost)) {
- errors.probeTargetHost = "Host 不能包含协议、空格或路径";
+ } else if (
+ probeHost.includes("://") ||
+ /[\s/?#]/.test(probeHost) ||
+ isSchemeLikeProbeHost(probeHost)
+ ) {
+ errors.probeTargetHost = "Host 不能包含协议、端口、空格或路径";
+ } else if (!isValidProbeTargetHost(probeHost)) {
+ errors.probeTargetHost = "测试目标 Host 格式无效";
}
if (!Number.isInteger(probePort) || probePort < 1 || probePort > 65535) {