公众号旧文
什么是 Looking Glass(lg)?
十几年前就看到过 Looking Glass 这个词,一直都懵懵懂懂的。
Looking Glass 直译过来是 观测镜,但我一直不知道是用来观测什么的。
比较官方的说法是:
Looking Glass 是基于 Web 的网络探测工具,可直接从当前节点发起网络诊断,帮助用户快速了解网络连通性、路由路径以及服务可用性。
说白了就是:
上线一台服务器,在网页上对外提供这个服务器的一些探测能力。
我这个站点上线已经很长时间了,长到我都忘了自己还有这么一个站。
http://45.202.240.112/
没用域名是因为,Looking Glass 观测的是从服务器到某个目标的结果。
比如用户自己的公网 IP,此时应该叫“服务器回程”。
而去程就需要服务器的 IP 了,于是我就直接把 IP 给放出来了。
目前支持以下功能:
- Ping:检测目标主机是否可达,并查看延迟、丢包率等网络质量信息。
- Traceroute:追踪数据包到目标主机的完整路由路径,展示每一跳的节点及往返延迟,方便分析网络出口和路由情况。
- DNS 查询:查询域名解析结果,验证 DNS 是否正常生效。
- HTTP 检测:获取目标网站的 HTTP/HTTPS 响应头,检查 Web 服务是否正常访问及返回状态。
- TCP 检测:检测指定 TCP 端口是否能够建立连接,用于验证常见服务端口的可达性。
为了保障服务的安全与稳定,本 Looking Glass 有多项安全机制,包括:
- 请求频率限制(Rate Limit)
- 最大并发控制
- SSRF 防护
- 私有地址及保留地址过滤
- 操作日志记录
所有探测均从当前节点发起,可用于排查不同地区节点之间的网络质量、路由路径及服务连通性。
本项目仍在持续完善中,后续将继续增加更多网络诊断功能。
比如:
- DNS 自动识别域名 / IP
- HTTP 显示所有解析到的 IP
- 给每个接口显示耗时
- Trace 支持 IPv6
欢迎体验并提出宝贵建议。
最后放一下源码:
from flask import Flask, request, jsonify, render_template_string
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
import subprocess
import ipaddress
import socket
import re
import time
from datetime import datetime
from threading import BoundedSemaphore, Lock
app = Flask(__name__)
limiter = Limiter(
get_remote_address,
app=app,
default_limits=["30 per minute"]
)
MAX_RUNNING = 10
SEM = BoundedSemaphore(MAX_RUNNING)
RUNNING_COUNT = 0
RUNNING_LOCK = Lock()
PRIVATE_NETS = [
ipaddress.ip_network("0.0.0.0/8"),
ipaddress.ip_network("10.0.0.0/8"),
ipaddress.ip_network("127.0.0.0/8"),
ipaddress.ip_network("169.254.0.0/16"),
ipaddress.ip_network("172.16.0.0/12"),
ipaddress.ip_network("192.168.0.0/16"),
ipaddress.ip_network("224.0.0.0/4"),
ipaddress.ip_network("240.0.0.0/4"),
ipaddress.ip_network("::1/128"),
ipaddress.ip_network("fc00::/7"),
ipaddress.ip_network("fe80::/10"),
]
DOMAIN_RE = re.compile(r"^[a-zA-Z0-9.-]{1,253}$")
ALLOWED_PORTS = {80, 443, 22, 25, 53}
INFO_CACHE = {"time": 0, "data": None}
def check_public_ip(ip_text):
ip = ipaddress.ip_address(ip_text)
for net in PRIVATE_NETS:
if ip in net:
raise ValueError("不允许探测内网、保留地址或本机地址")
return True
def resolve_all_ips(target):
target = target.strip()
if not DOMAIN_RE.match(target):
raise ValueError("目标格式不合法")
try:
ip = ipaddress.ip_address(target)
except ValueError:
ip = None
if ip is not None:
check_public_ip(str(ip))
return [str(ip)]
infos = socket.getaddrinfo(target, None, proto=socket.IPPROTO_TCP)
ips = sorted(set([i[4][0] for i in infos]))
if not ips:
raise ValueError("解析失败")
for ip in ips:
check_public_ip(ip)
return ips
def resolve_target(target):
return resolve_all_ips(target)[0]
def run_cmd(cmd, timeout=12):
global RUNNING_COUNT
with SEM:
with RUNNING_LOCK:
RUNNING_COUNT += 1
try:
p = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
timeout=timeout
)
return p.stdout[-12000:]
finally:
with RUNNING_LOCK:
RUNNING_COUNT -= 1
def client_ip():
return request.headers.get("X-Real-IP") or request.remote_addr or "-"
def write_log(kind, target, ip="-", port="-", ok=True):
line = (
f"{datetime.utcnow().isoformat()}Z\t"
f"{client_ip()}\t"
f"{kind}\t"
f"{target}\t"
f"{ip}\t"
f"{port}\t"
f"{ok}\n"
)
with open("/var/log/net-lg.log", "a", encoding="utf-8") as f:
f.write(line)
def safe_log(kind, target, ip="-", port="-", ok=True):
try:
write_log(kind, target, ip, port, ok)
except Exception:
pass
def get_node_info():
now = time.time()
if INFO_CACHE["data"] and now - INFO_CACHE["time"] < 600:
return INFO_CACHE["data"]
hostname = subprocess.check_output(
["hostname"],
text=True,
timeout=3
).strip()
geo_raw = subprocess.check_output(
["curl", "-s", "--max-time", "5", "myip.ipip.net"],
text=True,
timeout=6
).strip()
ip = ""
location = geo_raw
if "当前 IP:" in geo_raw:
ip = geo_raw.split("当前 IP:", 1)[1].split()[0].strip()
if "来自于:" in geo_raw:
location = geo_raw.split("来自于:", 1)[1].strip()
elif "来自于" in geo_raw:
location = geo_raw.split("来自于", 1)[1].strip()
data = {
"hostname": hostname,
"ip": ip,
"location": location,
"raw": geo_raw
}
INFO_CACHE["time"] = now
INFO_CACHE["data"] = data
return data
@app.route("/api/info")
def api_info():
try:
return jsonify(ok=True, **get_node_info())
except Exception as e:
return jsonify(ok=False, error=str(e)), 500
@app.route("/api/status")
def api_status():
with RUNNING_LOCK:
running = RUNNING_COUNT
return jsonify(
ok=True,
running=running,
max_running=MAX_RUNNING,
available=MAX_RUNNING - running
)
@app.route("/")
def index():
return render_template_string("""
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Network Looking Glass</title>
<style>
body { font-family: Arial, sans-serif; max-width: 900px; margin: 40px auto; padding: 0 20px; }
input, select, button { font-size: 16px; padding: 8px; }
input { width: 360px; }
pre { background: #111; color: #eee; padding: 16px; overflow: auto; border-radius: 8px; }
.card { border: 1px solid #ddd; padding: 20px; border-radius: 10px; }
#nodeinfo { color: #666; margin-bottom: 8px; }
#loadinfo { color: #666; margin-bottom: 15px; }
#type-buttons { margin: 10px 0; }
#type-buttons button { margin-right: 6px; padding: 8px 12px; cursor: pointer; border: 1px solid #ccc; background: #f7f7f7; border-radius: 6px; }
#type-buttons button.active { background: #222; color: white; border-color: #222; }
</style>
</head>
<body>
<h1>Network Looking Glass</h1>
<p id="nodeinfo">加载节点信息中...</p>
<p id="loadinfo">节点负载:加载中...</p>
<div class="card">
<p>从本节点发起 ping / traceroute / DNS / HTTP / TCP 查询。</p>
<input id="target" placeholder="example.com 或 8.8.8.8">
<input id="port" placeholder="端口(默认443)" style="width:120px; display:none;">
<div id="type-buttons">
<button type="button" data-type="ping">Ping</button>
<button type="button" data-type="trace">Trace</button>
<button type="button" data-type="dns">DNS</button>
<button type="button" data-type="http">HTTP</button>
<button type="button" data-type="tcp">TCP</button>
</div>
<button onclick="run()">开始探测</button>
</div>
<h3>结果</h3>
<pre id="out">等待输入...</pre>
<script>
async function loadInfo() {
try {
let r = await fetch("/api/info");
let j = await r.json();
if (j.ok) {
let text = "节点:" + j.hostname;
if (j.ip) text += " | 出口IP:" + j.ip;
if (j.location) text += " | 位置:" + j.location;
document.getElementById("nodeinfo").textContent = text;
} else {
document.getElementById("nodeinfo").textContent = "节点信息获取失败";
}
} catch (e) {
document.getElementById("nodeinfo").textContent = "节点信息获取失败";
}
}
async function loadStatus() {
try {
let r = await fetch("/api/status");
let j = await r.json();
if (j.ok) {
document.getElementById("loadinfo").textContent =
"节点负载:当前并发 " + j.running + " / " + j.max_running;
} else {
document.getElementById("loadinfo").textContent = "节点负载:获取失败";
}
} catch (e) {
document.getElementById("loadinfo").textContent = "节点负载:获取失败";
}
}
loadInfo();
loadStatus();
setInterval(loadStatus, 3000);
let currentType = "ping";
function updatePortBox() {
let portBox = document.getElementById("port");
portBox.style.display = currentType === "tcp" ? "inline-block" : "none";
}
function setType(type) {
currentType = type;
document.querySelectorAll("#type-buttons button").forEach(btn => {
if (btn.dataset.type === type) {
btn.classList.add("active");
} else {
btn.classList.remove("active");
}
});
updatePortBox();
}
document.querySelectorAll("#type-buttons button").forEach(btn => {
btn.addEventListener("click", function () {
setType(this.dataset.type);
});
});
setType("ping");
async function run() {
let target = document.getElementById("target").value;
let type = currentType;
let port = document.getElementById("port").value || "443";
let out = document.getElementById("out");
out.textContent = "运行中...";
let url = "/api/" + type + "?target=" + encodeURIComponent(target);
if (type === "tcp") {
url += "&port=" + encodeURIComponent(port);
}
try {
let r = await fetch(url);
if (r.status === 429) {
out.textContent = "⚠️ 请求过于频繁,请稍后再试(限速触发)";
loadStatus();
return;
}
let j = await r.json();
if (!r.ok) {
out.textContent = j.error
? ("错误: " + j.error)
: ("❌ 请求失败,HTTP 状态码:" + r.status);
loadStatus();
return;
}
out.textContent = j.ok ? j.output : ("错误: " + j.error);
loadStatus();
} catch (e) {
out.textContent = "❌ 网络错误:" + e;
loadStatus();
}
}
</script>
</body>
</html>
""")
@app.route("/api/ping")
@limiter.limit("10 per minute")
def api_ping():
target = request.args.get("target", "")
try:
ip = resolve_target(target)
output = run_cmd(["ping", "-c", "4", "-W", "2", ip], timeout=8)
safe_log("ping", target, ip, "-", True)
return jsonify(ok=True, target=target, ip=ip, output=output)
except Exception as e:
safe_log("ping", target, "-", "-", False)
return jsonify(ok=False, error=str(e)), 400
@app.route("/api/trace")
@limiter.limit("5 per minute")
def api_trace():
target = request.args.get("target", "")
try:
ip = resolve_target(target)
output = run_cmd(["traceroute", "-m", "20", "-w", "2", ip], timeout=15)
safe_log("trace", target, ip, "-", True)
return jsonify(ok=True, target=target, ip=ip, output=output)
except Exception as e:
safe_log("trace", target, "-", "-", False)
return jsonify(ok=False, error=str(e)), 400
@app.route("/api/dns")
@limiter.limit("20 per minute")
def api_dns():
target = request.args.get("target", "")
try:
if not DOMAIN_RE.match(target):
raise ValueError("域名格式不合法")
output = run_cmd(["dig", "+short", target], timeout=5)
safe_log("dns", target, "-", "-", True)
return jsonify(ok=True, target=target, output=output)
except Exception as e:
safe_log("dns", target, "-", "-", False)
return jsonify(ok=False, error=str(e)), 400
@app.route("/api/http")
@limiter.limit("10 per minute")
def api_http():
target = request.args.get("target", "").strip()
try:
if not DOMAIN_RE.match(target):
raise ValueError("目标格式不合法")
ips = resolve_all_ips(target)
ip = ips[0]
https_output = run_cmd(
[
"curl",
"-I",
"-sS",
"--max-time",
"6",
"--user-agent",
"Net-LG/1.0",
"--resolve",
f"{target}:443:{ip}",
f"https://{target}"
],
timeout=8
)
if "HTTP/" in https_output and "curl:" not in https_output:
safe_log("http", target, ip, 443, True)
return jsonify(
ok=True,
target=target,
ip=ip,
all_ips=ips,
protocol="https",
output="=== HTTPS ===\n" + https_output
)
http_output = run_cmd(
[
"curl",
"-I",
"-sS",
"--max-time",
"6",
"--user-agent",
"Net-LG/1.0",
"--resolve",
f"{target}:80:{ip}",
f"http://{target}"
],
timeout=8
)
ok = "HTTP/" in http_output
safe_log("http", target, ip, "443/80", ok)
return jsonify(
ok=True,
target=target,
ip=ip,
all_ips=ips,
protocol="http_fallback",
output="=== HTTPS 失败,尝试 HTTP ===\n"
+ https_output
+ "\n\n=== HTTP ===\n"
+ http_output
)
except Exception as e:
safe_log("http", target, "-", "-", False)
return jsonify(ok=False, error=str(e)), 400
@app.route("/api/tcp")
@limiter.limit("10 per minute")
def api_tcp():
target = request.args.get("target", "")
try:
port = int(request.args.get("port", "443"))
if port not in ALLOWED_PORTS:
raise ValueError("端口不允许")
ip = resolve_target(target)
output = run_cmd(
["nc", "-vz", "-w", "5", ip, str(port)],
timeout=6
)
safe_log("tcp", target, ip, port, True)
return jsonify(ok=True, target=target, ip=ip, port=port, output=output)
except Exception as e:
safe_log("tcp", target, "-", request.args.get("port", "-"), False)
return jsonify(ok=False, error=str(e)), 400
if __name__ == "__main__":
app.run(host="127.0.0.1", port=8090)