Skip to content

Commit 3d3f879

Browse files
committed
Add network snapshot insights
1 parent 200bbe5 commit 3d3f879

2 files changed

Lines changed: 116 additions & 4 deletions

File tree

webui/src/components/pages/NetworkSnapshotPanel.vue

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,42 @@
11
<script setup lang="ts">
2-
import { computed } from "vue";
3-
import { FileLock } from "lucide-vue-next";
2+
import { computed, ref, watch } from "vue";
3+
import { Copy, FileLock } from "lucide-vue-next";
4+
import Button from "@/components/ui/Button.vue";
45
import Card from "@/components/ui/Card.vue";
56
import { parseNetworkSnapshotSummary } from "@/composables/parsers";
7+
import { copyText } from "@/utils";
8+
import { buildNetworkSnapshotInsights, formatNetworkSnapshotReport, networkInsightTone } from "./networkSnapshotInsights";
69
710
const props = defineProps<{
811
topology: string;
912
sysroute: string;
1013
}>();
1114
12-
const snapshotText = computed(() => props.topology || props.sysroute);
15+
const snapshotText = computed(() => [
16+
props.topology ? `[topology]\n${props.topology}` : "",
17+
props.sysroute ? `[sysroute]\n${props.sysroute}` : ""
18+
].filter(Boolean).join("\n\n"));
1319
const summary = computed(() => parseNetworkSnapshotSummary(snapshotText.value));
20+
const insights = computed(() => buildNetworkSnapshotInsights(snapshotText.value));
21+
const copied = ref(false);
22+
23+
watch(snapshotText, () => {
24+
copied.value = false;
25+
});
26+
27+
async function copyNetworkReport(): Promise<void> {
28+
copied.value = await copyText(formatNetworkSnapshotReport(snapshotText.value, insights.value));
29+
}
1430
</script>
1531

1632
<template>
1733
<Card v-if="snapshotText">
18-
<h3 class="mb-2 inline-flex items-center gap-2 text-base font-semibold"><FileLock :size="17" /> 当前网络快照</h3>
34+
<div class="mb-2 flex flex-wrap items-start justify-between gap-2">
35+
<h3 class="inline-flex items-center gap-2 text-base font-semibold"><FileLock :size="17" /> 当前网络快照</h3>
36+
<Button size="sm" variant="outline" @click="copyNetworkReport">
37+
<Copy :size="15" />{{ copied ? "已复制报告" : "复制报告" }}
38+
</Button>
39+
</div>
1940
<div class="grid gap-2 sm:grid-cols-4">
2041
<div class="rounded-md border border-white/10 bg-white/5 p-3">
2142
<p class="text-xs text-zinc-500">接口</p>
@@ -34,6 +55,18 @@ const summary = computed(() => parseNetworkSnapshotSummary(snapshotText.value));
3455
<p class="text-lg font-semibold text-zinc-100">{{ summary.natRules }}</p>
3556
</div>
3657
</div>
58+
<div class="mt-2 grid gap-2 md:grid-cols-3">
59+
<div
60+
v-for="item in insights"
61+
:key="item.label"
62+
class="rounded-md border p-3"
63+
:class="networkInsightTone(item.tone)"
64+
>
65+
<p class="text-xs opacity-70">{{ item.label }}</p>
66+
<p class="mt-1 break-words text-sm font-semibold">{{ item.value }}</p>
67+
<p class="mt-1 break-words text-xs leading-5 opacity-70">{{ item.detail }}</p>
68+
</div>
69+
</div>
3770
<pre class="mt-2 max-h-[58vh] overflow-auto rounded-md bg-black p-3 text-xs leading-6 text-zinc-200 whitespace-pre-wrap">{{ snapshotText }}</pre>
3871
</Card>
3972
</template>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
export type NetworkSnapshotInsight = {
2+
label: string;
3+
value: string;
4+
detail: string;
5+
tone: "ok" | "warn" | "info";
6+
};
7+
8+
export function buildNetworkSnapshotInsights(text: string): NetworkSnapshotInsight[] {
9+
const lower = text.toLowerCase();
10+
const interfaces = collectInterfaceNames(text);
11+
const egress = interfaces.filter((name) => /^(wlan|wlp|enp|rmnet|ccmni|eth|usb|rndis|pdp|ap|swlan|wwan|cell|p2p|ppp)/i.test(name));
12+
const hasTun = interfaces.some((name) => /^(tun|utun|magicnet)/i.test(name)) || /\btun\b/i.test(text);
13+
const hasPolicyRule = hasSnapshotLine(text, /\bip rule:|from all fwmark\b|lookup \d+\b|lookup main\b/i);
14+
const hasNat = hasSnapshotLine(text, /\bmasquerade\b|\bsnat\b|\bdnat\b|-t nat\b|chain postrouting\b/i);
15+
const hasDnsRedirect = hasSnapshotLine(text, /\b(dpt:53|--dport 53|udp dpt:domain|tcp dpt:domain|redirect\b.*:53|to-ports (?:53|1053))\b/i);
16+
return [
17+
{
18+
label: "TUN",
19+
value: hasTun ? "detected" : "not detected",
20+
detail: hasTun ? "快照中出现 TUN/MagicNet 接口线索。" : "未看到 TUN 接口线索,透明代理可能未生效或快照不完整。",
21+
tone: hasTun ? "ok" : "warn"
22+
},
23+
{
24+
label: "出口接口",
25+
value: egress.length ? egress.slice(0, 4).join(", ") : "unknown",
26+
detail: egress.length ? "检测到常见出口接口命名线索。" : "未识别常见出口接口命名线索。",
27+
tone: egress.length ? "info" : "warn"
28+
},
29+
{
30+
label: "策略路由",
31+
value: hasPolicyRule ? "detected" : "not detected",
32+
detail: hasPolicyRule ? "快照文本包含 policy routing 线索。" : "未看到明显 ip rule/policy route 线索。",
33+
tone: hasPolicyRule ? "ok" : "warn"
34+
},
35+
{
36+
label: "NAT",
37+
value: hasNat ? "detected" : "not detected",
38+
detail: hasNat ? "快照文本包含 NAT/MASQUERADE 线索。" : "未看到 NAT 线索,热点转发或共享网络需继续确认。",
39+
tone: hasNat ? "ok" : "info"
40+
},
41+
{
42+
label: "DNS 捕获",
43+
value: hasDnsRedirect ? "detected" : "not detected",
44+
detail: hasDnsRedirect ? "快照文本包含 53 端口 redirect 线索。" : "未看到 DNS redirect 线索,可结合 DNS 工具继续测试。",
45+
tone: hasDnsRedirect ? "ok" : "info"
46+
},
47+
{
48+
label: "规模",
49+
value: `${interfaces.length} interfaces`,
50+
detail: `${text.split(/\r?\n/).filter((line) => line.trim()).length} 行有效快照文本。`,
51+
tone: lower.includes("error") || lower.includes("failed") ? "warn" : "info"
52+
}
53+
];
54+
}
55+
56+
export function formatNetworkSnapshotReport(text: string, insights: NetworkSnapshotInsight[]): string {
57+
const nonEmptyLines = text.split(/\r?\n/).filter((line) => line.trim()).length;
58+
return [
59+
"MagicNet network snapshot",
60+
`snapshot_lines=${nonEmptyLines}`,
61+
"raw_snapshot=omitted",
62+
...insights.map((item) => `${item.label}=${item.value} tone=${item.tone} detail=${item.detail}`),
63+
].join("\n").trim();
64+
}
65+
66+
export function networkInsightTone(tone: NetworkSnapshotInsight["tone"]): string {
67+
if (tone === "ok") return "border-lime-400/20 bg-lime-400/10 text-lime-100";
68+
if (tone === "warn") return "border-amber-400/30 bg-amber-400/10 text-amber-100";
69+
return "border-zinc-800 bg-zinc-950 text-zinc-300";
70+
}
71+
72+
function collectInterfaceNames(text: string): string[] {
73+
const names = text.match(/\b(?:wlan|wlp|enp|rmnet|ccmni|eth|usb|rndis|pdp|ap|swlan|wwan|cell|p2p|ppp|veth|br|bridge|tun|utun|lo|magicnet)[\w.:-]*/gi) || [];
74+
return Array.from(new Set(names.map((name) => name.replace(/:$/, ""))));
75+
}
76+
77+
function hasSnapshotLine(text: string, pattern: RegExp): boolean {
78+
return text.split(/\r?\n/).some((line) => pattern.test(line));
79+
}

0 commit comments

Comments
 (0)