Files
vpn-loopback-firewall/tools/analyze_capture.py
T

128 lines
4.4 KiB
Python

"""抓包验收分析: 软路由 WAN 口 tcpdump 抓的 pcap, 验证伪装/套壳效果。
用法: .venv/bin/python tools/analyze_capture.py <capture.pcap>
输出:
1. 总览(包数/字节/协议分布)
2. 出站 SYN 特征统计(TTL/window/wscale/TS/options) - L1 伪装器生效证据
3. 云通道(203.0.113.10:8444)流量统计 - 套壳证据
4. 明文 HTTP 出站(应 0, 全被套壳)与 DNS 出站(应 0, 走通道)检查
5. 目标端口/目标 IP Top
"出站"判定: 源 IP 在私有网段(192.168.0.0/16 等)视为内网侧。
"""
from __future__ import annotations
import sys
from collections import Counter
from pathlib import Path
from scapy.layers.inet import IP, TCP, UDP
from scapy.utils import rdpcap
CLOUD_IP = "203.0.113.10"
CLOUD_PORT = 8444
LAN_NETS = ("192.168.", "172.16.", "10.")
def _is_lan(ip: str) -> bool:
return any(ip.startswith(p) for p in LAN_NETS)
def _tcp_options(tcp: TCP) -> dict[str, object]:
"""scapy option kind 是字符串名(MSS/SAckOK/Timestamp/NOP/WScale/EOL)。"""
opts: dict[str, object] = {}
if not tcp.options:
return opts
for entry in tcp.options:
if isinstance(entry, tuple) and len(entry) >= 1 and isinstance(entry[0], str):
opts[entry[0]] = entry[1] if len(entry) > 1 else None
return opts
def main() -> int:
if len(sys.argv) < 2:
print("usage: python tools/analyze_capture.py <capture.pcap>", file=sys.stderr)
return 2
path = Path(sys.argv[1])
if not path.exists():
print(f"文件不存在: {path}", file=sys.stderr)
return 2
pkts = rdpcap(str(path))
total = len(pkts)
bytes_total = sum(len(bytes(p)) for p in pkts)
print(f"== 总览: {total} 包 / {bytes_total/1024:.1f} KB")
proto: Counter[int] = Counter()
out_syn: list[tuple[int, int, int, bool, list[str], int, str]] = []
cloud_bytes = 0
cloud_pkts = 0
plain_http_out = 0
dns_out = 0
dst_ports: Counter[int] = Counter()
dst_ips: Counter[str] = Counter()
for p in pkts:
if IP not in p:
continue
ip = p[IP]
proto[ip.proto] += 1
src, dst = ip.src, ip.dst
outbound = _is_lan(src) and not _is_lan(dst)
if dst == CLOUD_IP or src == CLOUD_IP:
cloud_pkts += 1
cloud_bytes += len(p)
if TCP in p:
tcp = p[TCP]
dst_ports[tcp.dport] += 1
dst_ips[dst] += 1
if outbound and tcp.flags & 0x02 and not tcp.flags & 0x10:
opts = _tcp_options(tcp)
ws = opts.get("WScale")
wscale_v = ws if isinstance(ws, int) else -1
kinds = list(opts.keys())
out_syn.append(
(ip.ttl, tcp.window, wscale_v, "Timestamp" in opts, kinds, len(p), dst)
)
if outbound and tcp.dport == 80:
plain_http_out += 1
elif UDP in p:
udp = p[UDP]
dst_ports[udp.dport] += 1
if outbound and udp.dport == 53:
dns_out += 1
print("\n== 出站 SYN 特征(L1 伪装器生效证据)")
if out_syn:
ttl_c = Counter(s[0] for s in out_syn)
win_c = Counter(s[1] for s in out_syn)
ws_c = Counter(s[2] for s in out_syn)
ts_c = Counter(s[3] for s in out_syn)
print(f"SYN 总数: {len(out_syn)}")
print(f"TTL 分布: {dict(ttl_c)} (期望 128 统一)")
print(f"window 分布: {dict(win_c)} (期望 64240 统一)")
print(f"wscale 分布: {dict(ws_c)} (期望 8 统一)")
print(f"带 TS option: {dict(ts_c)} (期望全部 False - 已剥)")
seen_orders: Counter[tuple[str, ...]] = Counter(tuple(s[4]) for s in out_syn)
print(f"options 顺序(前 3 种): {seen_orders.most_common(3)}")
else:
print("无出站 SYN(抓包期间无新连接?)")
print("\n== 云通道(套壳证据)")
print(f"到/从 {CLOUD_IP}:{CLOUD_PORT} 的包: {cloud_pkts} 个 / {cloud_bytes/1024:.1f} KB")
print("\n== 明文泄漏检查(期望全 0)")
print(f"明文 HTTP 出站(dport 80): {plain_http_out}")
print(f"公网 DNS 出站(dport 53): {dns_out}")
print("\n== 目标端口 Top 8")
for port, n in dst_ports.most_common(8):
print(f" {port}: {n}")
print("== 目标 IP Top 8")
for ip, n in dst_ips.most_common(8):
print(f" {ip}: {n}")
return 0
if __name__ == "__main__":
raise SystemExit(main())