#!/bin/bash # 从两个隧道日志里统计 QUIC vs HTTP/2 的连接行为,按 edge IP 聚合。 # # 目的:判断 QUIC dial 失败到底是"某些 IP 不可达"还是"UDP 整体被限制"。 # 如果失败集中在少数 IP -> 是那些 IP 的问题,可以避开 # 如果所有 IP 都失败 -> 是 UDP 整体被运营商限制,只能靠 HTTP/2 兜底 set -u MAIN=/root/lanremote-relay/tunnel.log H2=/root/lanremote-relay/tunnel-h2.log echo "════════ 1. QUIC 实例(tunnel.log)════════" echo " 日志时间跨度: $(head -1 "$MAIN" 2>/dev/null | cut -c1-19) ~ $(tail -1 "$MAIN" 2>/dev/null | cut -c1-19)" echo " 总行数: $(wc -l < "$MAIN")" echo echo " ── 各类事件计数 ──" for kw in 'Registered tunnel connection' 'Failed to dial' 'Lost connection' \ 'Unregistered' 'no recent network activity' 'Retrying connection' \ 'Serve tunnel error' 'context deadline' 'i/o timeout'; do n=$(grep -c "$kw" "$MAIN" 2>/dev/null || echo 0) printf " %-34s %s\n" "$kw" "$n" done echo echo " ── Failed to dial 按 IP 聚合(谁的失败最多)──" grep 'Failed to dial' "$MAIN" 2>/dev/null \ | grep -oE 'ip=[0-9.]+' | sort | uniq -c | sort -rn | head -12 | sed 's/^/ /' echo echo " ── 成功注册按 IP 聚合 ──" grep 'Registered tunnel connection' "$MAIN" 2>/dev/null \ | grep -oE 'ip=[0-9.]+' | sort | uniq -c | sort -rn | head -12 | sed 's/^/ /' echo echo " ── 成功注册按落点聚合 ──" grep 'Registered tunnel connection' "$MAIN" 2>/dev/null \ | grep -oE 'location=[a-z0-9]+' | sort | uniq -c | sort -rn | head -12 | sed 's/^/ /' echo echo "════════ 2. HTTP/2 实例(tunnel-h2.log)════════" echo " 日志时间跨度: $(head -1 "$H2" 2>/dev/null | cut -c1-19) ~ $(tail -1 "$H2" 2>/dev/null | cut -c1-19)" echo " 总行数: $(wc -l < "$H2")" echo for kw in 'Registered tunnel connection' 'Failed to dial' 'Lost connection' \ 'Unregistered' 'Retrying connection' 'i/o timeout' 'context deadline'; do n=$(grep -c "$kw" "$H2" 2>/dev/null || echo 0) printf " %-34s %s\n" "$kw" "$n" done echo echo " ── 成功注册按落点聚合 ──" grep 'Registered tunnel connection' "$H2" 2>/dev/null \ | grep -oE 'location=[a-z0-9]+' | sort | uniq -c | sort -rn | head -10 | sed 's/^/ /' echo echo "════════ 3. 两者的「注册/失败」比率对比 ════════" for pair in "QUIC:$MAIN" "HTTP2:$H2"; do name="${pair%%:*}"; f="${pair##*:}" reg=$(grep -c 'Registered tunnel connection' "$f" 2>/dev/null || echo 0) fail=$(grep -c 'Failed to dial' "$f" 2>/dev/null || echo 0) lost=$(grep -c 'Lost connection' "$f" 2>/dev/null || echo 0) tot=$((reg + fail)) pct=0 [ "$tot" -gt 0 ] && pct=$((reg * 100 / tot)) printf " %-6s 注册 %4d 失败 %4d 掉线 %3d 成功率 %d%%\n" \ "$name" "$reg" "$fail" "$lost" "$pct" done echo echo "════════ 4. 失败的时间分布(按 10 分钟分桶)════════" echo " QUIC 的 Failed to dial:" grep 'Failed to dial' "$MAIN" 2>/dev/null \ | sed -E 's/^([0-9-]+T[0-9]{2}:[0-9]).*/\1/' \ | sed -E 's/(T[0-9]{2}:[0-9])[0-9]/\10/' \ | sort | uniq -c | tail -20 | sed 's/^/ /' echo echo "════════ 5. UDP 可达性实测(对 Cloudflare edge 的 7844/UDP)════════" python3 - <<'PYEOF' import socket, time, struct, os # QUIC 的 Initial 包要正确构造才会有回应;这里用一个最简的 # "版本协商探测":发一个短 header,看有没有任何 UDP 回应。 # 关键判断不是"内容对不对",而是**有没有任何包回来** —— # 如果连 ICMP port-unreachable 都没有,说明 UDP 被完全丢掉了。 IPS = ["198.41.192.227", "198.41.200.23", "198.41.200.193", "198.41.200.43", "198.41.200.63", "198.41.192.57", "198.41.192.47", "198.41.200.33", "198.41.192.107", "198.41.192.67", "198.41.192.167", "198.41.200.113"] def tcp_probe(ip, port=7844, timeout=3): s = socket.socket(); s.settimeout(timeout) t = time.perf_counter() try: s.connect((ip, port)) return (time.perf_counter() - t) * 1000, True except OSError: return (time.perf_counter() - t) * 1000, False finally: s.close() def udp_probe(ip, port=7844, timeout=3): """发一个 QUIC 长头 Initial 形状的包,等任何回应。""" s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.settimeout(timeout) t = time.perf_counter() try: # 最小可解析的 QUIC long header:flags=0xc0, version=1, # dcid len=8, dcid, scid len=0, token len=0, length=8, payload pkt = b"\xc0\x00\x00\x00\x01" + b"\x08" + os.urandom(8) + b"\x00" \ + b"\x00" + struct.pack(">H", 8) + os.urandom(8) s.sendto(pkt, (ip, port)) data, _addr = s.recvfrom(2048) return (time.perf_counter() - t) * 1000, True except socket.timeout: return (time.perf_counter() - t) * 1000, False except OSError as e: # ICMP port unreachable 会以 ConnectionRefusedError 冒出来 return (time.perf_counter() - t) * 1000, ("refused" in str(e).lower()) finally: s.close() tcp_ok = 0; udp_ok = 0 print(" %-16s %-18s %s" % ("IP", "TCP:7844", "UDP:7844")) for ip in IPS: tms, tok = tcp_probe(ip) ums, uok = udp_probe(ip) tcp_ok += 1 if tok else 0 udp_ok += 1 if uok else 0 print(" %-16s %-18s %s" % ( ip, "通 %.0fms" % tms if tok else "不通", "通 %.0fms" % ums if uok else "无回应")) print() print(" TCP 可达 %d/%d UDP 有回应 %d/%d" % (tcp_ok, len(IPS), udp_ok, len(IPS))) print(" 结论: UDP 有回应数远少于 TCP -> UDP 被链路限制,QUIC 先天不利") PYEOF