06470512f6
### 严重修复 1. 跨服务器无法登录 - 新增SESSION_COOKIE_DOMAIN/SECURE/NAME配置 2. SOCKS5用户密码明文存储→bcrypt哈希(兼容自动迁移) 3. 仪表盘审计日志不显示→替换为空循环为真实API加载 4. 管理员登录防爆破未生效→auth.py集成check_fail2ban ### 中等问题修复 5. 流量趋势图表无数据→新增record_traffic_snapshot后台线程 6. 网络流量累计值跳变→新增基线差值机制 7. asyncio连接计数非线程安全→加锁保护 ### 优化改进 8. API密钥页面截断文本修复 9. 新增gunicorn/bcrypt依赖 10. 更新README环境变量文档和生产建议 11. 登录密码常量时间对比防时序攻击 12. 仪表盘图表添加animation:false防卡顿
251 lines
8.9 KiB
Python
251 lines
8.9 KiB
Python
"""监控与统计服务。"""
|
|
import copy
|
|
import logging
|
|
import os
|
|
import psutil
|
|
from collections import deque
|
|
from datetime import datetime, timezone, timedelta
|
|
|
|
from models import TrafficSnapshot, Connection, AuditLog, Instance, User
|
|
from database import db
|
|
|
|
log = logging.getLogger("socks.stats")
|
|
|
|
# 实时指标缓冲区(内存)
|
|
_metrics_buffer = deque(maxlen=600) # 10分钟 @ 10秒间隔
|
|
_metrics_lock = __import__("threading").Lock()
|
|
|
|
# 网络流量基线(用于计算会话增量)
|
|
_net_baseline = None
|
|
_net_baseline_lock = __import__("threading").Lock()
|
|
|
|
|
|
def _get_net_delta():
|
|
global _net_baseline
|
|
try:
|
|
net = psutil.net_io_counters()
|
|
current = {"bytes_sent": net.bytes_sent, "bytes_recv": net.bytes_recv}
|
|
with _net_baseline_lock:
|
|
if _net_baseline is None:
|
|
_net_baseline = copy.deepcopy(current)
|
|
return {"bytes_sent": 0, "bytes_recv": 0}
|
|
return {
|
|
"bytes_sent": current["bytes_sent"] - _net_baseline["bytes_sent"],
|
|
"bytes_recv": current["bytes_recv"] - _net_baseline["bytes_recv"],
|
|
}
|
|
except Exception:
|
|
return {"bytes_sent": 0, "bytes_recv": 0}
|
|
|
|
|
|
def _record_metrics():
|
|
"""记录系统级指标。"""
|
|
try:
|
|
cpu = psutil.cpu_percent(interval=0.5)
|
|
mem = psutil.virtual_memory()
|
|
net_delta = _get_net_delta()
|
|
# 计算活跃连接
|
|
with db.app.app_context():
|
|
active = db.session.query(Connection).filter_by(
|
|
state="active"
|
|
).count()
|
|
item = {
|
|
"ts": datetime.now(timezone.utc).isoformat(),
|
|
"cpu": cpu,
|
|
"mem_percent": mem.percent,
|
|
"mem_used_mb": mem.used // 1024 // 1024,
|
|
"mem_total_mb": mem.total // 1024 // 1024,
|
|
"net_bytes_sent": net_delta["bytes_sent"],
|
|
"net_bytes_recv": net_delta["bytes_recv"],
|
|
"active_connections": active,
|
|
}
|
|
with _metrics_lock:
|
|
_metrics_buffer.append(item)
|
|
except Exception as e:
|
|
log.error("记录指标失败: %s", e)
|
|
|
|
|
|
def get_realtime_metrics():
|
|
"""获取实时系统指标。"""
|
|
_record_metrics()
|
|
with _metrics_lock:
|
|
items = list(_metrics_buffer)
|
|
if not items:
|
|
_record_metrics()
|
|
items = list(_metrics_buffer)
|
|
return {
|
|
"latest": items[-1] if items else None,
|
|
"history": items[-60:], # 最近60个点(10分钟)
|
|
}
|
|
|
|
|
|
def get_traffic_trend(days=30):
|
|
"""获取流量趋势(每日快照)。"""
|
|
with db.app.app_context():
|
|
start = datetime.now(timezone.utc).date() - timedelta(days=days)
|
|
snapshots = db.session.query(
|
|
TrafficSnapshot.date,
|
|
db.func.sum(TrafficSnapshot.bytes_in).label("total_in"),
|
|
db.func.sum(TrafficSnapshot.bytes_out).label("total_out"),
|
|
db.func.sum(TrafficSnapshot.connections).label("total_conn"),
|
|
).filter(
|
|
TrafficSnapshot.date >= start
|
|
).group_by(TrafficSnapshot.date).order_by(
|
|
TrafficSnapshot.date
|
|
).all()
|
|
return [{
|
|
"date": s.date.isoformat(),
|
|
"bytes_in_mb": round((s.total_in or 0) / 1024 / 1024, 1),
|
|
"bytes_out_mb": round((s.total_out or 0) / 1024 / 1024, 1),
|
|
"connections": s.total_conn or 0,
|
|
} for s in snapshots]
|
|
|
|
|
|
def get_user_stats():
|
|
"""获取各用户流量排名。"""
|
|
from sqlalchemy import func
|
|
with db.app.app_context():
|
|
users = db.session.query(
|
|
User.username,
|
|
User.bytes_in,
|
|
User.bytes_out,
|
|
User.max_concurrent,
|
|
User.total_traffic_mb,
|
|
(User.bytes_in + User.bytes_out).label("total_bytes"),
|
|
).filter_by(enabled=True).order_by(
|
|
(User.bytes_in + User.bytes_out).desc()
|
|
).limit(20).all()
|
|
return [{
|
|
"username": u.username,
|
|
"bytes_in_mb": round(u.bytes_in / 1024 / 1024, 1),
|
|
"bytes_out_mb": round(u.bytes_out / 1024 / 1024, 1),
|
|
"total_mb": round(u.total_bytes / 1024 / 1024, 1),
|
|
"limit_mb": u.total_traffic_mb,
|
|
"usage_pct": round(
|
|
u.total_bytes / 1024 / 1024 / u.total_traffic_mb * 100, 1
|
|
) if u.total_traffic_mb else None,
|
|
} for u in users]
|
|
|
|
|
|
def get_instance_stats():
|
|
"""获取各实例流量统计。"""
|
|
with db.app.app_context():
|
|
instances = Instance.query.all()
|
|
result = []
|
|
for inst in instances:
|
|
conns = db.session.query(
|
|
db.func.sum(Connection.bytes_in).label("total_in"),
|
|
db.func.sum(Connection.bytes_out).label("total_out"),
|
|
db.func.count(Connection.id).label("total_conn"),
|
|
).filter(
|
|
Connection.instance_id == inst.id
|
|
).first()
|
|
result.append({
|
|
"id": inst.id,
|
|
"name": inst.name,
|
|
"host": inst.listen_host,
|
|
"port": inst.listen_port,
|
|
"bytes_in_mb": round((conns.total_in or 0) / 1024 / 1024, 1),
|
|
"bytes_out_mb": round((conns.total_out or 0) / 1024 / 1024, 1),
|
|
"total_connections": conns.total_conn or 0,
|
|
})
|
|
return result
|
|
|
|
|
|
def get_active_connections():
|
|
"""获取当前活跃连接列表。"""
|
|
with db.app.app_context():
|
|
conns = db.session.query(Connection).filter_by(
|
|
state="active"
|
|
).order_by(Connection.started_at.desc()).limit(50).all()
|
|
return [{
|
|
"id": c.id,
|
|
"instance": db.session.query(Instance).get(c.instance_id).name if c.instance_id else "",
|
|
"username": c.username,
|
|
"src": f"{c.src_ip}:{c.src_port}" if c.src_ip else "",
|
|
"dst": f"{c.dst_addr}:{c.dst_port}" if c.dst_addr else "",
|
|
"protocol": c.protocol,
|
|
"started": c.started_at.isoformat() if c.started_at else "",
|
|
"bytes_in": c.bytes_in,
|
|
"bytes_out": c.bytes_out,
|
|
} for c in conns]
|
|
|
|
|
|
def get_dashboard_summary():
|
|
"""仪表盘汇总。"""
|
|
metrics = get_realtime_metrics()
|
|
with db.app.app_context():
|
|
total_instances = db.session.query(Instance).count()
|
|
running_instances = db.session.query(Instance).filter_by(
|
|
enabled=True
|
|
).count()
|
|
total_users = db.session.query(User).count()
|
|
active_users = db.session.query(User).filter_by(
|
|
enabled=True, banned=False
|
|
).count()
|
|
active_conns = db.session.query(Connection).filter_by(
|
|
state="active"
|
|
).count()
|
|
today_logs = db.session.query(AuditLog).filter(
|
|
AuditLog.timestamp >= datetime.now(timezone.utc).replace(
|
|
hour=0, minute=0, second=0
|
|
)
|
|
).count()
|
|
return {
|
|
"system": metrics["latest"],
|
|
"total_instances": total_instances,
|
|
"running_instances": running_instances,
|
|
"total_users": total_users,
|
|
"active_users": active_users,
|
|
"active_connections": active_conns,
|
|
"today_logs": today_logs,
|
|
}
|
|
|
|
|
|
def prune_old_data():
|
|
"""清理旧数据(运行在后台)。"""
|
|
with db.app.app_context():
|
|
# 清理 30 天前的连接记录(closed的)
|
|
cutoff = datetime.now(timezone.utc) - timedelta(days=30)
|
|
db.session.query(Connection).filter(
|
|
Connection.state == "closed",
|
|
Connection.started_at < cutoff
|
|
).delete(synchronize_session=False)
|
|
# 清理 90 天前的审计日志
|
|
cutoff2 = datetime.now(timezone.utc) - timedelta(days=90)
|
|
db.session.query(AuditLog).filter(AuditLog.timestamp < cutoff2).delete(
|
|
synchronize_session=False
|
|
)
|
|
db.session.commit()
|
|
log.info("数据清理完成")
|
|
|
|
|
|
def record_traffic_snapshot():
|
|
"""记录每日流量快照(用于流量趋势图表)。"""
|
|
from datetime import date
|
|
today = date.today()
|
|
with db.app.app_context():
|
|
from sqlalchemy import func
|
|
today_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
|
|
result = db.session.query(
|
|
func.coalesce(func.sum(Connection.bytes_in), 0),
|
|
func.coalesce(func.sum(Connection.bytes_out), 0),
|
|
func.count(Connection.id),
|
|
).filter(Connection.started_at >= today_start).first()
|
|
total_in = result[0] if result else 0
|
|
total_out = result[1] if result else 0
|
|
total_conns = result[2] if result else 0
|
|
|
|
snap = db.session.query(TrafficSnapshot).filter_by(date=today).first()
|
|
if snap:
|
|
snap.bytes_in = total_in
|
|
snap.bytes_out = total_out
|
|
snap.connections = total_conns
|
|
else:
|
|
snap = TrafficSnapshot(
|
|
date=today, bytes_in=total_in, bytes_out=total_out,
|
|
connections=total_conns,
|
|
)
|
|
db.session.add(snap)
|
|
db.session.commit()
|
|
log.info("流量快照已记录: %s", today)
|