Compare commits

...

17 Commits

Author SHA1 Message Date
yyhuni
3ba1ba427e fix: agent自动更新逻辑 2025-12-19 19:48:01 +08:00
yyhuni
6019555729 fix:ssl问题 2025-12-19 19:41:12 +08:00
github-actions[bot]
750f52c515 chore: bump version to v1.0.20 2025-12-19 11:28:27 +00:00
yyhuni
bb5ce66a31 fix:agent容器版本号匹配 2025-12-19 19:20:15 +08:00
github-actions[bot]
ac958571a5 chore: bump version to v1.0.19 2025-12-19 11:12:14 +00:00
yyhuni
bcb321f883 Merge branch 'main' of https://github.com/yyhuni/xingrin 2025-12-19 19:03:39 +08:00
yyhuni
fd3cdf8033 fix:远程worker 8888端口问题 2025-12-19 19:02:43 +08:00
github-actions[bot]
f3f9718df2 chore: bump version to v1.0.18 2025-12-19 10:47:10 +00:00
yyhuni
984c34dbca 优化:取消暴漏8888端口 2025-12-19 18:37:05 +08:00
yyhuni
e9dcbf510d 更新readme 2025-12-19 16:19:11 +08:00
yyhuni
65deb8c5d0 更新文档 2025-12-19 16:15:57 +08:00
yyhuni
5a93ad878c 更新架构图文档 2025-12-19 16:05:32 +08:00
github-actions[bot]
51f25d0976 chore: bump version to v1.0.17 2025-12-19 04:50:17 +00:00
yyhuni
fe1579e7fb 优化 :负载逻辑,高负载时先等待,给系统喘息时间 2025-12-19 12:42:15 +08:00
yyhuni
ef117d2245 fix:交换分区开启命令 2025-12-19 12:33:48 +08:00
yyhuni
39cea5a918 增加:脚本一键开启交换分区 2025-12-19 12:30:02 +08:00
github-actions[bot]
0d477ce269 chore: bump version to v1.0.16 2025-12-19 04:23:00 +00:00
24 changed files with 787 additions and 262 deletions

View File

@@ -104,6 +104,8 @@ jobs:
tags: |
${{ env.IMAGE_PREFIX }}/${{ matrix.image }}:${{ steps.version.outputs.VERSION }}
${{ steps.version.outputs.IS_RELEASE == 'true' && format('{0}/{1}:latest', env.IMAGE_PREFIX, matrix.image) || '' }}
build-args: |
IMAGE_TAG=${{ steps.version.outputs.VERSION }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false

137
README.md
View File

@@ -31,6 +31,7 @@
- [🔄 版本管理](./docs/version-management.md) - Git Tag 驱动的自动化版本管理系统
- [📦 Nuclei 模板架构](./docs/nuclei-template-architecture.md) - 模板仓库的存储与同步
- [📖 字典文件架构](./docs/wordlist-architecture.md) - 字典文件的存储与同步
- [🔍 扫描流程架构](./docs/scan-flow-architecture.md) - 完整扫描流程与工具编排
---
@@ -48,6 +49,54 @@
- **自定义流程** - YAML 配置扫描流程,灵活编排
- **定时扫描** - Cron 表达式配置,自动化周期扫描
#### 扫描流程架构
完整的扫描流程包括子域名发现、端口扫描、站点发现、URL 收集、目录扫描、漏洞扫描等阶段
```mermaid
flowchart LR
START["开始扫描"]
subgraph STAGE1["阶段 1: 资产发现"]
direction TB
SUB["子域名发现<br/>subfinder, amass, puredns"]
PORT["端口扫描<br/>naabu"]
SITE["站点识别<br/>httpx"]
SUB --> PORT --> SITE
end
subgraph STAGE2["阶段 2: 深度分析"]
direction TB
URL["URL 收集<br/>waymore, katana"]
DIR["目录扫描<br/>ffuf"]
end
subgraph STAGE3["阶段 3: 漏洞检测"]
VULN["漏洞扫描<br/>nuclei, dalfox"]
end
FINISH["扫描完成"]
START --> STAGE1
SITE --> STAGE2
STAGE2 --> STAGE3
STAGE3 --> FINISH
style START fill:#34495e,stroke:#2c3e50,stroke-width:2px,color:#fff
style FINISH fill:#27ae60,stroke:#229954,stroke-width:2px,color:#fff
style STAGE1 fill:#3498db,stroke:#2980b9,stroke-width:2px,color:#fff
style STAGE2 fill:#9b59b6,stroke:#8e44ad,stroke-width:2px,color:#fff
style STAGE3 fill:#e67e22,stroke:#d35400,stroke-width:2px,color:#fff
style SUB fill:#5dade2,stroke:#3498db,stroke-width:1px,color:#fff
style PORT fill:#5dade2,stroke:#3498db,stroke-width:1px,color:#fff
style SITE fill:#5dade2,stroke:#3498db,stroke-width:1px,color:#fff
style URL fill:#bb8fce,stroke:#9b59b6,stroke-width:1px,color:#fff
style DIR fill:#bb8fce,stroke:#9b59b6,stroke-width:1px,color:#fff
style VULN fill:#f0b27a,stroke:#e67e22,stroke-width:1px,color:#fff
```
详细说明请查看 [扫描流程架构文档](./docs/scan-flow-architecture.md)
### 🖥️ 分布式架构
- **多节点扫描** - 支持部署多个 Worker 节点,横向扩展扫描能力
- **本地节点** - 零配置,安装即自动注册本地 Docker Worker
@@ -56,34 +105,41 @@
- **节点监控** - 实时心跳检测CPU/内存/磁盘状态监控
- **断线重连** - 节点离线自动检测,恢复后自动重新接入
```
┌─────────────────────────────────────────────────────────────────┐
│ 主服务器 (Master) │
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Next.js │ │ Django │ │ Postgres│ │ Redis │
│ 前端 │ │ 后端 │ │ 数据库 │ │ 缓存 │ │
│ └─────────┘ └────┬────┘ └─────────┘ └─────────┘
│ │
┌─────┴─────┐ │
│ 任务调度器 │ │
│ Scheduler │ │
└─────┬─────┘ │
└────────────────────┼────────────────────────────────────────────┘
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
Worker 1 │ │ Worker 2 │ │ Worker N │
│ (本地) │ │ (远程) │ │ (远程) │
├───────────┤ ├───────────┤ ├───────────┤
│ • Nuclei │ │ • Nuclei │ │ • Nuclei │
│ • httpx │ │ • httpx │ │ • httpx │
│ • naabu │ │ • naabu │ │ • naabu │
│ • ... │ │ • ... │ │ • ... │
├───────────┤ ├───────────┤ ├───────────┤
心跳上报 │ │ 心跳上报 │ │ 心跳上报 │
└───────────┘ └───────────┘ └───────────┘
```mermaid
flowchart TB
subgraph MASTER["主服务器 (Master Server)"]
direction TB
REDIS["Redis 负载缓存"]
subgraph SCHEDULER["任务调度器 (Task Distributor)"]
direction TB
SUBMIT["接收扫描任务"]
SELECT["负载感知选择"]
DISPATCH["智能分发"]
SUBMIT --> SELECT
SELECT --> DISPATCH
end
REDIS -.负载数据.-> SELECT
end
subgraph WORKERS["Worker 节点集群"]
direction TB
W1["Worker 1 (本地)<br/>CPU: 45% | MEM: 60%"]
W2["Worker 2 (远程)<br/>CPU: 30% | MEM: 40%"]
W3["Worker N (远程)<br/>CPU: 90% | MEM: 85%"]
end
DISPATCH -->|任务分发| W1
DISPATCH -->|任务分发| W2
DISPATCH -->|高负载跳过| W3
W1 -.心跳上报.-> REDIS
W2 -.心跳上报.-> REDIS
W3 -.心跳上报.-> REDIS
```
### 📊 可视化界面
@@ -100,27 +156,12 @@
- **数据库**: PostgreSQL + Redis
- **部署**: Docker + Nginx
### 🔧 内置扫描工具
| 类别 | 工具 |
|------|------|
| 子域名爆破 | puredns, massdns, dnsgen |
| 被动发现 | subfinder, amass, assetfinder, Sublist3r |
| 端口扫描 | naabu |
| 站点发现 | httpx |
| 目录扫描 | ffuf |
| 爬虫 | katana |
| 被动URL收集 | waymore, uro |
| 漏洞扫描 | nuclei, dalfox |
---
## 📦 快速开始
### 环境要求
- **操作系统**: Ubuntu 20.04+ / Debian 11+ (推荐)
- **硬件**: 2核 4G 内存起步,10GB+ 磁盘空间
- **硬件**: 2核 4G 内存起步,20GB+ 磁盘空间
### 一键安装
@@ -131,14 +172,11 @@ cd xingrin
# 安装并启动(生产模式)
sudo ./install.sh
# 开发模式
sudo ./install.sh --dev
```
### 访问服务
- **Web 界面**: `https://localhost` `http://localhost`
- **Web 界面**: `https://localhost`
### 常用命令
@@ -158,9 +196,6 @@ sudo ./uninstall.sh
# 更新
sudo ./update.sh
```
## 日志
- 项目日志:/opt/xingrin/logs 下存储了这个项目的运行日志信息error文件存储了错误相关信息xingrin.log存储了包括错误在内的所有项目日志
- 工具调用日志:/opt/xingrin/results 下存储了工具的运行结果日志比如naabuhttpx等的结果调用日志
## 🤝 反馈与贡献

View File

@@ -1 +1 @@
v1.0.14
v1.0.20

View File

@@ -14,6 +14,10 @@ import os
import sys
import requests
import logging
import urllib3
# 禁用自签名证书的 SSL 警告(远程 Worker 场景)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
logger = logging.getLogger(__name__)
@@ -36,7 +40,8 @@ def fetch_config_and_setup_django():
print(f"[CONFIG] 正在从配置中心获取配置: {config_url}")
print(f"[CONFIG] IS_LOCAL={is_local}")
try:
resp = requests.get(config_url, timeout=10)
# verify=False: 远程 Worker 通过 HTTPS 访问时可能使用自签名证书
resp = requests.get(config_url, timeout=10, verify=False)
resp.raise_for_status()
config = resp.json()

View File

@@ -241,8 +241,9 @@ class WorkerDeployConsumer(AsyncWebsocketConsumer):
}))
return
django_host = f"{public_host}:{server_port}" # Django / 心跳上报使用
heartbeat_api_url = f"http://{django_host}" # 基础 URLagent 会加 /api/...
# 远程 Worker 通过 nginx HTTPS 访问nginx 反代到后端 8888
# 使用 https://{PUBLIC_HOST} 而不是直连 8888 端口
heartbeat_api_url = f"https://{public_host}" # 基础 URLagent 会加 /api/...
session_name = f'xingrin_deploy_{self.worker_id}'
remote_script_path = '/tmp/xingrin_deploy.sh'

View File

@@ -153,12 +153,18 @@ class TaskDistributor:
else:
scored_workers.append((worker, score, cpu, mem))
# 降级策略:如果没有正常负载的,使用高负载中最低的
# 降级策略:如果没有正常负载的,等待后重新选择
if not scored_workers:
if high_load_workers:
logger.warning("所有 Worker 高负载,降级选择负载最低的")
# 高负载时先等待,给系统喘息时间(默认 60 秒)
high_load_wait = getattr(settings, 'HIGH_LOAD_WAIT_SECONDS', 60)
logger.warning("所有 Worker 高负载,等待 %d 秒后重试...", high_load_wait)
time.sleep(high_load_wait)
# 重新选择(递归调用,可能负载已降下来)
# 为避免无限递归,这里直接使用高负载中最低的
high_load_workers.sort(key=lambda x: x[1])
best_worker, score, cpu, mem = high_load_workers[0]
best_worker, _, cpu, mem = high_load_workers[0]
# 发送高负载通知
from apps.common.signals import all_workers_high_load
@@ -169,10 +175,7 @@ class TaskDistributor:
mem=mem
)
logger.info(
"选择 Worker: %s (CPU: %.1f%%, MEM: %.1f%%, Score: %.1f)",
best_worker.name, cpu, mem, score
)
logger.info("选择 Worker: %s (CPU: %.1f%%, MEM: %.1f%%)", best_worker.name, cpu, mem)
return best_worker
else:
logger.warning("没有可用的 Worker")
@@ -229,9 +232,9 @@ class TaskDistributor:
network_arg = f"--network {settings.DOCKER_NETWORK_NAME}"
server_url = f"http://server:{settings.SERVER_PORT}"
else:
# 远程:无需指定网络,使用公网地址
# 远程:通过 Nginx 反向代理访问HTTPS不直连 8888 端口)
network_arg = ""
server_url = f"http://{settings.PUBLIC_HOST}:{settings.SERVER_PORT}"
server_url = f"https://{settings.PUBLIC_HOST}"
# 挂载路径(所有节点统一使用固定路径)
host_results_dir = settings.HOST_RESULTS_DIR # /opt/xingrin/results

View File

@@ -134,5 +134,57 @@ class WorkerService:
logger.warning(f"[卸载] Worker {worker_id} 远程卸载异常: {e}")
return False, f"远程卸载异常: {str(e)}"
def execute_remote_command(
self,
ip_address: str,
ssh_port: int,
username: str,
password: str | None,
command: str
) -> tuple[bool, str]:
"""
在远程主机上执行命令
Args:
ip_address: SSH 主机地址
ssh_port: SSH 端口
username: SSH 用户名
password: SSH 密码
command: 要执行的命令
Returns:
(success, message) 元组
"""
if not password:
return False, "未配置 SSH 密码"
try:
import paramiko
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(
ip_address,
port=ssh_port,
username=username,
password=password,
timeout=30
)
stdin, stdout, stderr = ssh.exec_command(command, timeout=120)
exit_status = stdout.channel.recv_exit_status()
ssh.close()
if exit_status == 0:
return True, stdout.read().decode().strip()
else:
error = stderr.read().decode().strip()
return False, error
except Exception as e:
return False, str(e)
__all__ = ["WorkerService"]

View File

@@ -118,8 +118,25 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['post'])
def heartbeat(self, request, pk=None):
"""接收心跳上报(写 Redis首次心跳更新部署状态"""
"""
接收心跳上报(写 Redis首次心跳更新部署状态检查版本
请求体:
{
"cpu_percent": 50.0,
"memory_percent": 60.0,
"version": "v1.0.9"
}
返回:
{
"status": "ok",
"need_update": true/false,
"server_version": "v1.0.19"
}
"""
from apps.engine.services.worker_load_service import worker_load_service
from django.conf import settings
worker = self.get_object()
info = request.data if request.data else {}
@@ -134,7 +151,96 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
worker.status = 'online'
worker.save(update_fields=['status'])
return Response({'status': 'ok'})
# 3. 版本检查:比较 agent 版本与 server 版本
agent_version = info.get('version', '')
server_version = settings.IMAGE_TAG # Server 当前版本
need_update = False
if agent_version and agent_version != 'unknown':
# 版本不匹配时通知 agent 更新
need_update = agent_version != server_version
if need_update:
logger.info(
f"Worker {worker.name} 版本不匹配: agent={agent_version}, server={server_version}"
)
# 远程 Worker服务端主动通过 SSH 触发更新
# 旧版 agent 不会解析 need_update所以需要服务端主动推送
if not worker.is_local and worker.ip_address:
self._trigger_remote_agent_update(worker, server_version)
return Response({
'status': 'ok',
'need_update': need_update,
'server_version': server_version
})
def _trigger_remote_agent_update(self, worker, target_version: str):
"""
通过 SSH 触发远程 agent 更新(后台执行,不阻塞心跳响应)
使用 Redis 锁防止重复触发(同一 worker 60秒内只触发一次
"""
import redis
from django.conf import settings as django_settings
redis_client = redis.from_url(django_settings.REDIS_URL)
lock_key = f"agent_update_lock:{worker.id}"
# 尝试获取锁60秒过期防止重复触发
if not redis_client.set(lock_key, "1", nx=True, ex=60):
logger.debug(f"Worker {worker.name} 更新已在进行中,跳过")
return
# 提取数据避免后台线程访问 ORM
worker_id = worker.id
worker_name = worker.name
ip_address = worker.ip_address
ssh_port = worker.ssh_port
username = worker.username
password = worker.password
def _async_update():
try:
logger.info(f"开始远程更新 Worker {worker_name}{target_version}")
# 构建更新命令:拉取新镜像并重启 agent
docker_user = getattr(django_settings, 'DOCKER_USER', 'yyhuni')
update_cmd = f'''
docker pull {docker_user}/xingrin-agent:{target_version} && \
docker stop xingrin-agent 2>/dev/null || true && \
docker rm xingrin-agent 2>/dev/null || true && \
docker run -d --pull=always \
--name xingrin-agent \
--restart always \
-e HEARTBEAT_API_URL="https://{django_settings.PUBLIC_HOST}" \
-e WORKER_ID="{worker_id}" \
-e IMAGE_TAG="{target_version}" \
-v /proc:/host/proc:ro \
{docker_user}/xingrin-agent:{target_version}
'''
success, message = self.worker_service.execute_remote_command(
ip_address=ip_address,
ssh_port=ssh_port,
username=username,
password=password,
command=update_cmd
)
if success:
logger.info(f"Worker {worker_name} 远程更新成功")
else:
logger.warning(f"Worker {worker_name} 远程更新失败: {message}")
except Exception as e:
logger.error(f"Worker {worker_name} 远程更新异常: {e}")
finally:
# 释放锁
redis_client.delete(lock_key)
# 后台执行,不阻塞心跳响应
threading.Thread(target=_async_update, daemon=True).start()
@action(detail=False, methods=['post'])
def register(self, request):

View File

@@ -372,19 +372,17 @@ def port_scan_flow(
端口扫描 Flow
主要功能:
1. 扫描目标域名的开放端口(核心目标)
2. 发现域名对应的 IP 地址(附带产物)
3. 保存 IP 和端口的关联关系
1. 扫描目标域名/IP 的开放端口
2. 保存 host + ip + port 三元映射到 HostPortMapping 表
输出资产:
- Port开放的端口列表主要资产
- IPAddress域名对应的 IP 地址(附带资产)
- HostPortMapping主机端口映射host + ip + port 三元组
工作流程:
Step 0: 创建工作目录
Step 1: 导出域名列表到文件(供扫描工具使用)
Step 2: 解析配置,获取启用的工具
Step 3: 串行执行扫描工具,运行端口扫描工具并实时解析输出到数据库(Subdomain → IPAddress → Port
Step 3: 串行执行扫描工具,运行端口扫描工具并实时解析输出到数据库(→ HostPortMapping
Args:
scan_id: 扫描任务 ID
@@ -418,10 +416,8 @@ def port_scan_flow(
RuntimeError: 执行失败
Note:
端口扫描的输出必然包含 IP 信息,因为:
- 扫描工具需要解析域名 → IP
- 端口属于 IP而不是直接属于域名
- 同一域名可能对应多个 IPCDN、负载均衡
端口扫描工具(如 naabu会解析域名获取 IP输出 host + ip + port 三元组。
同一 host 可能对应多个 IPCDN、负载均衡因此使用三元映射表存储。
"""
try:
# 参数验证

View File

@@ -1,5 +1,5 @@
"""
子域名发现扫描 Flow(增强版)
子域名发现扫描 Flow
负责编排子域名发现扫描的完整流程
@@ -343,7 +343,7 @@ def subdomain_discovery_flow(
scan_workspace_dir: str,
enabled_tools: dict
) -> dict:
"""子域名发现扫描流程(增强版)
"""子域名发现扫描流程
工作流程4 阶段):
Stage 1: 被动收集(并行) - 必选
@@ -410,7 +410,7 @@ def subdomain_discovery_flow(
# 验证成功后打印日志
logger.info(
"="*60 + "\n" +
"开始子域名发现扫描(增强版)\n" +
"开始子域名发现扫描\n" +
f" Scan ID: {scan_id}\n" +
f" Domain: {domain_name}\n" +
f" Workspace: {scan_workspace_dir}\n" +

View File

@@ -3,10 +3,14 @@
import logging
import time
import requests
import urllib3
from .models import Notification, NotificationSettings
from .types import NotificationLevel, NotificationCategory
from .repositories import DjangoNotificationRepository, NotificationSettingsRepository
# 禁用自签名证书的 SSL 警告(远程 Worker 回调场景)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
logger = logging.getLogger(__name__)
@@ -314,7 +318,8 @@ def _push_via_api_callback(notification: Notification, server_url: str) -> None:
'created_at': notification.created_at.isoformat()
}
resp = requests.post(callback_url, json=data, timeout=5)
# verify=False: 远程 Worker 回调 Server 时可能使用自签名证书
resp = requests.post(callback_url, json=data, timeout=5, verify=False)
resp.raise_for_status()
logger.debug(f"通知回调推送成功 - ID: {notification.id}")

View File

@@ -7,6 +7,7 @@
import logging
import os
import ssl
from pathlib import Path
from urllib import request as urllib_request
from urllib import parse as urllib_parse
@@ -81,15 +82,20 @@ def ensure_wordlist_local(wordlist_name: str) -> str:
raise RuntimeError(
"无法确定 Django API 地址:请配置 SERVER_URL 或 PUBLIC_HOST 环境变量"
)
server_port = getattr(settings, 'SERVER_PORT', '8888')
api_base = f"http://{public_host}:{server_port}/api"
# 远程 Worker 通过 nginx HTTPS 访问,不再直连 8888
api_base = f"https://{public_host}/api"
query = urllib_parse.urlencode({'wordlist': wordlist_name})
download_url = f"{api_base.rstrip('/')}/wordlists/download/?{query}"
logger.info("从后端下载字典: %s -> %s", download_url, local_path)
try:
with urllib_request.urlopen(download_url) as resp:
# 创建不验证 SSL 的上下文(远程 Worker 可能使用自签名证书)
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
with urllib_request.urlopen(download_url, context=ssl_context) as resp:
if resp.status != 200:
raise RuntimeError(f"下载字典失败HTTP {resp.status}")
data = resp.read()

View File

@@ -1,7 +1,7 @@
#!/bin/bash
# ============================================
# XingRin Agent
# 用途:心跳上报 + 负载监控
# 用途:心跳上报 + 负载监控 + 版本检查
# 适用:远程 VPS 或 Docker 容器内
# ============================================
@@ -17,6 +17,9 @@ SRC_DIR="${MARKER_DIR}/src"
ENV_FILE="${SRC_DIR}/backend/.env"
INTERVAL=${AGENT_INTERVAL:-3}
# Agent 版本(从环境变量获取,由 Docker 镜像构建时注入)
AGENT_VERSION="${IMAGE_TAG:-unknown}"
# 颜色定义
GREEN='\033[0;32m'
RED='\033[0;31m'
@@ -52,7 +55,7 @@ if [ "$RUN_MODE" = "remote" ] && [ -f "$ENV_FILE" ]; then
fi
# 获取配置
# SERVER_URL: 后端 API 地址(容器内用 http://server:8888远程用公网地址
# SERVER_URL: 后端 API 地址(容器内用 http://server:8888远程用 https://{PUBLIC_HOST}
API_URL="${HEARTBEAT_API_URL:-${SERVER_URL:-}}"
WORKER_NAME="${WORKER_NAME:-}"
IS_LOCAL="${IS_LOCAL:-false}"
@@ -90,7 +93,7 @@ register_worker() {
EOF
)
RESPONSE=$(curl -s -X POST \
RESPONSE=$(curl -k -s -X POST \
-H "Content-Type: application/json" \
-d "$REGISTER_DATA" \
"${API_URL}/api/workers/register/" 2>/dev/null)
@@ -113,7 +116,7 @@ if [ -z "$WORKER_ID" ]; then
# 等待 Server 就绪
log "等待 Server 就绪..."
for i in $(seq 1 30); do
if curl -s "${API_URL}/api/" > /dev/null 2>&1; then
if curl -k -s "${API_URL}/api/" > /dev/null 2>&1; then
log "${GREEN}Server 已就绪${NC}"
break
fi
@@ -172,22 +175,72 @@ while true; do
fi
# 构建 JSON 数据(使用数值而非字符串,便于比较和排序)
# 包含版本号,供 Server 端检查版本一致性
JSON_DATA=$(cat <<EOF
{
"cpu_percent": $CPU_PERCENT,
"memory_percent": $MEM_PERCENT
"memory_percent": $MEM_PERCENT,
"version": "$AGENT_VERSION"
}
EOF
)
# 发送心跳
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
# 发送心跳,获取响应内容
RESPONSE_FILE=$(mktemp)
HTTP_CODE=$(curl -k -s -o "$RESPONSE_FILE" -w "%{http_code}" -X POST \
-H "Content-Type: application/json" \
-d "$JSON_DATA" \
"${API_URL}/api/workers/${WORKER_ID}/heartbeat/" 2>/dev/null || echo "000")
RESPONSE_BODY=$(cat "$RESPONSE_FILE" 2>/dev/null)
rm -f "$RESPONSE_FILE"
if [ "$RESPONSE" != "200" ] && [ "$RESPONSE" != "201" ]; then
log "${YELLOW}心跳发送失败 (HTTP $RESPONSE)${NC}"
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then
log "${YELLOW}心跳发送失败 (HTTP $HTTP_CODE)${NC}"
else
# 检查是否需要更新
NEED_UPDATE=$(echo "$RESPONSE_BODY" | grep -oE '"need_update":\s*(true|false)' | grep -oE '(true|false)')
if [ "$NEED_UPDATE" = "true" ]; then
SERVER_VERSION=$(echo "$RESPONSE_BODY" | grep -oE '"server_version":\s*"[^"]+"' | sed 's/.*"\([^"]*\)"$/\1/')
log "${YELLOW}检测到版本不匹配: Agent=$AGENT_VERSION, Server=$SERVER_VERSION${NC}"
log "${GREEN}正在自动更新...${NC}"
# 执行自动更新
if [ "$RUN_MODE" = "container" ]; then
# 容器模式:通知外部重启(退出后由 docker-compose restart policy 重启)
log "容器模式:退出以触发重启更新"
exit 0
else
# 远程模式:拉取新镜像并重启 agent 容器
log "远程模式:更新 agent 镜像..."
DOCKER_USER="${DOCKER_USER:-yyhuni}"
NEW_IMAGE="${DOCKER_USER}/xingrin-agent:${SERVER_VERSION}"
# 拉取新镜像
if $DOCKER_CMD pull "$NEW_IMAGE" 2>/dev/null; then
log "${GREEN}镜像拉取成功: $NEW_IMAGE${NC}"
# 停止当前容器并用新镜像重启
CONTAINER_NAME="xingrin-agent"
$DOCKER_CMD stop "$CONTAINER_NAME" 2>/dev/null || true
$DOCKER_CMD rm "$CONTAINER_NAME" 2>/dev/null || true
# 重新启动(使用相同的环境变量)
$DOCKER_CMD run -d \
--name "$CONTAINER_NAME" \
--restart unless-stopped \
-e HEARTBEAT_API_URL="$API_URL" \
-e WORKER_ID="$WORKER_ID" \
-e IMAGE_TAG="$SERVER_VERSION" \
-v /proc:/host/proc:ro \
"$NEW_IMAGE"
log "${GREEN}Agent 已更新到 $SERVER_VERSION${NC}"
exit 0
else
log "${RED}镜像拉取失败: $NEW_IMAGE${NC}"
fi
fi
fi
fi
# 休眠

View File

@@ -60,12 +60,14 @@ start_agent() {
log_info "=========================================="
log_info "启动 agent 容器..."
# --pull=missing 只在本地没有镜像时才拉取,避免意外更新
# --pull=missing: 本地没有镜像时才拉取
# 版本更新由服务端通过 SSH 显式 docker pull 触发
docker run -d --pull=missing \
--name ${CONTAINER_NAME} \
--restart always \
-e SERVER_URL="${PRESET_SERVER_URL}" \
-e WORKER_ID="${PRESET_WORKER_ID}" \
-e IMAGE_TAG="${IMAGE_TAG}" \
-v /proc:/host/proc:ro \
${IMAGE}

View File

@@ -15,14 +15,14 @@ REDIS_PORT=6379
REDIS_DB=0
# ==================== 服务端口配置 ====================
# SERVER_PORT 为 Django / uvicorn 对外端口
# SERVER_PORT 为 Django / uvicorn 容器内部端口(由 nginx 反代,对公网不直接暴露)
SERVER_PORT=8888
# ==================== 远程 Worker 配置 ====================
# 供远程 Worker 访问主服务器的地址:
# - 仅本地部署serverDocker 内部服务名)
# - 有远程 Worker改为主服务器外网 IP如 192.168.1.100
# 注意:远程 Worker 访问数据库/Redis 也会使用此地址(除非配置了远程 PostgreSQL
# - 有远程 Worker改为主服务器外网 IP 或域名(如 192.168.1.100 或 xingrin.example.com
# 注意:远程 Worker 会通过 https://{PUBLIC_HOST} 访问nginx 反代到后端 8888
PUBLIC_HOST=server
# ==================== Django 核心配置 ====================

View File

@@ -1,12 +1,15 @@
# ============================================
# XingRin Agent - 轻量心跳上报镜像
# 用途:心跳上报 + 负载监控
# 用途:心跳上报 + 负载监控 + 版本检查
# 基础镜像Alpine Linux (~5MB)
# 最终大小:~10MB
# ============================================
FROM alpine:3.19
# 构建参数:版本号
ARG IMAGE_TAG=unknown
# 安装必要工具
RUN apk add --no-cache \
bash \
@@ -17,6 +20,9 @@ RUN apk add --no-cache \
COPY backend/scripts/worker-deploy/agent.sh /app/agent.sh
RUN chmod +x /app/agent.sh
# 将版本号写入环境变量(运行时可用)
ENV IMAGE_TAG=${IMAGE_TAG}
# 工作目录
WORKDIR /app

View File

@@ -37,8 +37,6 @@ services:
context: ..
dockerfile: docker/server/Dockerfile
restart: always
ports:
- "${SERVER_PORT}:8888"
env_file:
- .env
depends_on:
@@ -56,19 +54,19 @@ services:
retries: 3
start_period: 60s
# Agent心跳上报 + 负载监控
# Agent心跳上报 + 负载监控 + 版本检查
agent:
build:
context: ..
dockerfile: docker/worker/Dockerfile
dockerfile: docker/agent/Dockerfile
args:
IMAGE_TAG: ${IMAGE_TAG:-dev}
restart: always
env_file:
- .env
environment:
- SERVER_URL=http://server:8888
- WORKER_NAME=本地节点
- IS_LOCAL=true
command: bash /app/backend/scripts/worker-deploy/agent.sh
- IMAGE_TAG=${IMAGE_TAG:-dev}
depends_on:
server:
condition: service_healthy

View File

@@ -41,8 +41,6 @@ services:
server:
image: ${DOCKER_USER:-yyhuni}/xingrin-server:${IMAGE_TAG:?IMAGE_TAG is required}
restart: always
ports:
- "${SERVER_PORT}:8888"
env_file:
- .env
depends_on:
@@ -74,6 +72,7 @@ services:
- SERVER_URL=http://server:8888
- WORKER_NAME=本地节点
- IS_LOCAL=true
- IMAGE_TAG=${IMAGE_TAG}
depends_on:
server:
condition: service_healthy

97
docker/scripts/setup-swap.sh Executable file
View File

@@ -0,0 +1,97 @@
#!/bin/bash
#
# Ubuntu/Debian 一键开启交换分区脚本
# 用法: sudo ./setup-swap.sh [大小GB]
# 示例: sudo ./setup-swap.sh 4 # 创建 4GB 交换分区
# sudo ./setup-swap.sh # 默认创建与内存相同大小的交换分区
#
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# 检查 root 权限
if [ "$EUID" -ne 0 ]; then
log_error "请使用 sudo 运行此脚本"
exit 1
fi
# 检查是否已有交换分区
CURRENT_SWAP_KB=$(grep SwapTotal /proc/meminfo | awk '{print $2}')
CURRENT_SWAP_GB=$(awk "BEGIN {printf \"%.0f\", $CURRENT_SWAP_KB / 1024 / 1024}")
if [ "$CURRENT_SWAP_GB" -gt 0 ]; then
log_warn "系统已有 ${CURRENT_SWAP_GB}GB 交换分区"
swapon --show
read -p "是否继续添加新的交换分区?(y/N) " -r
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log_info "已取消"
exit 0
fi
fi
# 获取系统内存大小GB四舍五入
TOTAL_MEM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}')
TOTAL_MEM_GB=$(awk "BEGIN {printf \"%.0f\", $TOTAL_MEM_KB / 1024 / 1024}")
# 确定交换分区大小
if [ -n "$1" ]; then
SWAP_SIZE_GB=$1
else
# 默认与内存相同,最小 1GB最大 8GB
SWAP_SIZE_GB=$TOTAL_MEM_GB
[ "$SWAP_SIZE_GB" -lt 1 ] && SWAP_SIZE_GB=1
[ "$SWAP_SIZE_GB" -gt 8 ] && SWAP_SIZE_GB=8
fi
SWAP_FILE="/swapfile_xingrin"
log_info "系统内存: ${TOTAL_MEM_GB}GB"
log_info "将创建 ${SWAP_SIZE_GB}GB 交换分区: $SWAP_FILE"
# 检查磁盘空间(向下取整,保守估计)
AVAILABLE_GB=$(df / | tail -1 | awk '{printf "%.0f", $4/1024/1024}')
if [ "$AVAILABLE_GB" -lt "$SWAP_SIZE_GB" ]; then
log_error "磁盘空间不足!可用: ${AVAILABLE_GB}GB需要: ${SWAP_SIZE_GB}GB"
exit 1
fi
# 创建交换文件
log_info "正在创建交换文件(可能需要几分钟)..."
dd if=/dev/zero of=$SWAP_FILE bs=1G count=$SWAP_SIZE_GB status=progress
# 设置权限
chmod 600 $SWAP_FILE
# 格式化为交换分区
mkswap $SWAP_FILE
# 启用交换分区
swapon $SWAP_FILE
# 添加到 fstab开机自动挂载
if ! grep -q "$SWAP_FILE" /etc/fstab; then
echo "$SWAP_FILE none swap sw 0 0" >> /etc/fstab
log_info "已添加到 /etc/fstab开机自动启用"
fi
# 优化 swappiness降低交换倾向优先使用内存
SWAPPINESS=10
if ! grep -q "vm.swappiness" /etc/sysctl.conf; then
echo "vm.swappiness=$SWAPPINESS" >> /etc/sysctl.conf
fi
sysctl vm.swappiness=$SWAPPINESS >/dev/null
log_info "交换分区创建成功!"
echo ""
echo "当前交换分区状态:"
swapon --show
echo ""
free -h

View File

@@ -135,6 +135,7 @@ if [ "$DEV_MODE" = true ]; then
fi
else
# 生产模式:拉取 Docker Hub 镜像
# pull 后 up -d 会自动检测镜像变化并重建容器
if [ "$WITH_FRONTEND" = true ]; then
echo -e "${CYAN}[PULL]${NC} 拉取最新镜像..."
${COMPOSE_CMD} ${COMPOSE_ARGS} pull
@@ -173,7 +174,7 @@ if [ "$WITH_FRONTEND" = true ]; then
echo -e " XingRin: ${CYAN}https://${ACCESS_HOST}/${NC}"
echo -e " ${YELLOW}(HTTP 会自动跳转到 HTTPS)${NC}"
else
echo -e " API: ${CYAN}http://${ACCESS_HOST}:8888${NC}"
echo -e " API: ${CYAN}通过前端或 nginx 访问(后端未暴露 8888${NC}"
echo ""
echo -e "${YELLOW}[TIP]${NC} 前端未启动,请手动运行:"
echo " cd frontend && pnpm dev"

View File

@@ -14,10 +14,9 @@
- **端口要求**: 需要开放以下端口
- `80` - HTTP 访问(自动跳转到 HTTPS
- `443` - HTTPS 访问(主要访问端口)
- `3000` - 前端开发服务(开发模式)
- `8888` - 后端 API 服务
- `5432` - PostgreSQL 数据库(如使用本地数据库)
- `6379` - Redis 缓存服务
- 后端 API 仅容器内监听 8888由 nginx 反代到 80/443对公网无需放行 8888
## 一键安装
@@ -64,10 +63,10 @@ sudo ./install.sh --no-frontend
80 - HTTP 访问
443 - HTTPS 访问
3000 - 前端服务(开发模式)
8888 - 后端 API
5432 - PostgreSQL如使用本地数据库
6379 - Redis 缓存
```
> 后端 API 默认仅在容器内 8888 监听,由 nginx 反代到 80/443对公网无需放行 8888。
#### 推荐方案
- **国外 VPS**:如 Vultr、DigitalOcean、Linode 等,默认开放所有端口,无需额外配置
@@ -157,8 +156,8 @@ DB_USER=postgres # 数据库用户
DB_PASSWORD=随机生成 # 数据库密码
# 服务配置
SERVER_PORT=8888 # 后端服务端口
PUBLIC_HOST=server # 对外访问地址
SERVER_PORT=8888 # 后端容器内部端口(仅 Docker 内网监听)
PUBLIC_HOST=server # 对外访问地址(远程 Worker 用,配置外网 IP 或域名)
DEBUG=False # 调试模式
# 版本配置

View File

@@ -0,0 +1,123 @@
# 扫描流程架构
## 完整扫描流程
```mermaid
flowchart TB
START[Start Scan]
TARGET[Input Target]
START --> TARGET
subgraph STAGE1["Stage 1: Discovery Sequential"]
direction TB
subgraph SUB["Subdomain Discovery"]
direction TB
SUBFINDER[subfinder]
AMASS[amass]
SUBLIST3R[sublist3r]
ASSETFINDER[assetfinder]
MERGE[Merge & Deduplicate]
BRUTEFORCE[puredns bruteforce<br/>Dictionary Attack]
MUTATE[dnsgen + puredns<br/>Mutation Generation]
RESOLVE[puredns resolve<br/>Alive Verification]
SUBFINDER --> MERGE
AMASS --> MERGE
SUBLIST3R --> MERGE
ASSETFINDER --> MERGE
MERGE --> BRUTEFORCE
BRUTEFORCE --> MUTATE
MUTATE --> RESOLVE
end
subgraph PORT["Port Scan"]
NAABU[naabu<br/>Port Discovery]
end
subgraph SITE["Site Scan"]
HTTPX1[httpx<br/>Web Service Detection]
end
RESOLVE --> NAABU
NAABU --> HTTPX1
end
TARGET --> SUBFINDER
TARGET --> AMASS
TARGET --> SUBLIST3R
TARGET --> ASSETFINDER
subgraph STAGE2["Stage 2: Analysis Parallel"]
direction TB
subgraph URL["URL Collection"]
direction TB
WAYMORE[waymore<br/>Historical URLs]
KATANA[katana<br/>Crawler]
URO[uro<br/>URL Deduplication]
HTTPX2[httpx<br/>Alive Verification]
WAYMORE --> URO
KATANA --> URO
URO --> HTTPX2
end
subgraph DIR["Directory Scan"]
FFUF[ffuf<br/>Directory Bruteforce]
end
end
HTTPX1 --> WAYMORE
HTTPX1 --> KATANA
HTTPX1 --> FFUF
subgraph STAGE3["Stage 3: Vulnerability Sequential"]
direction TB
subgraph VULN["Vulnerability Scan"]
direction LR
DALFOX[dalfox<br/>XSS Scan]
NUCLEI[nuclei<br/>Vulnerability Scan]
end
end
HTTPX2 --> DALFOX
HTTPX2 --> NUCLEI
DALFOX --> FINISH
NUCLEI --> FINISH
FFUF --> FINISH
FINISH[Scan Complete]
style START fill:#ff9999
style FINISH fill:#99ff99
style TARGET fill:#ffcc99
style STAGE1 fill:#e6f3ff
style STAGE2 fill:#fff4e6
style STAGE3 fill:#ffe6f0
```
## 执行阶段定义
```python
# backend/apps/scan/configs/command_templates.py
EXECUTION_STAGES = [
{'mode': 'sequential', 'flows': ['subdomain_discovery', 'port_scan', 'site_scan']},
{'mode': 'parallel', 'flows': ['url_fetch', 'directory_scan']},
{'mode': 'sequential', 'flows': ['vuln_scan']},
]
```
## 各阶段输出
| Flow | 工具 | 输出表 |
|------|------|--------|
| subdomain_discovery | subfinder, amass, sublist3r, assetfinder, puredns | Subdomain |
| port_scan | naabu | HostPortMapping |
| site_scan | httpx | WebSite |
| url_fetch | waymore, katana, uro, httpx | Endpoint |
| directory_scan | ffuf | Directory |
| vuln_scan | dalfox, nuclei | Vulnerability |

View File

@@ -40,62 +40,66 @@ Wordlist
- 统计文件大小和行数
- 创建数据库记录
```
┌──────────────────────────────────────────────────────────────────────────┐
│ Server 容器 │
│ │
┌─────────────────────────────────────────────────────────────────┐
前端 UI │ │
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ 上传字典 │ 编辑内容 │ │ 删除字典 │ │ │
│ 选择文件 │ 在线修改 │ │ │ │ │
└──────┬───────┘ └──────┬───────┘ └──────────────┘ │ │
└─────────┼───────────────────┼───────────────────────────────────┘
│ │ │
│ ▼ ▼ │
┌─────────────────────────────────────────────────────────────────┐ │
│ │ WordlistViewSet │ │
│ │ POST /api/wordlists/ | PUT .../content/ │ │
└─────────────────────────────┬───────────────────────────────────┘ │
│ │ │
▼ │
┌─────────────────────────────────────────────────────────────────┐ │
│ │ WordlistService │ │
│ │ │ │
┌────────────────────┐ ┌────────────────────────────────┐ │ │
│ create_wordlist() update_wordlist_content() │ │ │
│ │ │ 创建字典 │ │ 更新字典内容 │ │ │
└────────┬───────────┘ └───────────────┬────────────────┘ │ │
└───────────┼────────────────────────────────┼────────────────────┘ │
│ │ │
▼ ▼ │
┌─────────────────────────────────────────────────────────────────┐ │
│ │ 处理流程 │ │
│ │ │ │
1. 保存文件到 /opt/xingrin/wordlists/<filename> │ │
│ │ 2. 计算 SHA256 哈希值 │ │
│ 3. 统计文件大小和行数 │ │
│ │ 4. 创建/更新数据库记录 │ │
└─────────────────────────────────────────────────────────────────┘ │
│ │
┌─────────────────────────────────────────────────────────────────┐ │
│ │ PostgreSQL 数据库 │ │
│ │ │ │
INSERT INTO wordlist (name, file_path, file_size, │ │
│ │ line_count, file_hash) │ │
VALUES ('subdomains', '/opt/xingrin/wordlists/subdomains.txt', │ │
│ │ 1024000, 50000, 'sha256...') │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
┌─────────────────────────────────────────────────────────────────┐
│ │ 文件系统 │ │
/opt/xingrin/wordlists/ │ │
├── common.txt │ │
├── subdomains.txt │ │
│ │ └── directories.txt │ │
└─────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘
```mermaid
flowchart TB
subgraph SERVER["🖥️ Server 容器"]
direction TB
subgraph UI["前端 UI"]
direction LR
UPLOAD["📤 上传字典<br/>选择文件"]
EDIT["✏️ 编辑内容<br/>在线修改"]
DELETE["🗑️ 删除字典"]
end
UPLOAD --> API
EDIT --> API
subgraph API["API 层"]
VIEWSET["WordlistViewSet<br/>POST /api/wordlists/<br/>PUT .../content/"]
end
API --> SERVICE
subgraph SERVICE["业务逻辑层"]
CREATE["create_wordlist()<br/>创建字典"]
UPDATE["update_wordlist_content()<br/>更新字典内容"]
end
CREATE --> PROCESS
UPDATE --> PROCESS
subgraph PROCESS["处理流程"]
direction TB
STEP1["1⃣ 保存文件到<br/>/opt/xingrin/wordlists/"]
STEP2["2⃣ 计算 SHA256 哈希值"]
STEP3["3⃣ 统计文件大小和行数"]
STEP4["4⃣ 创建/更新数据库记录"]
STEP1 --> STEP2
STEP2 --> STEP3
STEP3 --> STEP4
end
STEP4 --> DB
STEP1 --> FS
subgraph DB["💾 PostgreSQL 数据库"]
DBRECORD["INSERT INTO wordlist<br/>name: 'subdomains'<br/>file_path: '/opt/xingrin/wordlists/subdomains.txt'<br/>file_size: 1024000<br/>line_count: 50000<br/>file_hash: 'sha256...'"]
end
subgraph FS["📁 文件系统"]
FILES["/opt/xingrin/wordlists/<br/>├── common.txt<br/>├── subdomains.txt<br/>└── directories.txt"]
end
end
style SERVER fill:#e6f3ff
style UI fill:#fff4e6
style API fill:#f0f0f0
style SERVICE fill:#d4edda
style PROCESS fill:#ffe6f0
style DB fill:#cce5ff
style FS fill:#e2e3e5
```
## 四、Worker 端获取流程
@@ -110,76 +114,65 @@ Worker 执行扫描任务时,通过 `ensure_wordlist_local()` 获取字典:
3. 下载地址:`GET /api/wordlists/download/?wordlist=<name>`
4. 返回本地字典文件路径
```
┌──────────────────────────────────────────────────────────────────────────┐
│ Worker 容器 │
│ │
│ ┌─────────────┐ │
│ │ 扫描任务 │ │
│ │ 需要字典 │ │
│ └──────┬──────┘ │
│ │
▼ │
┌─────────────────────────┐ ┌─────────────────────────────────┐ │
│ ensure_wordlist_local() │ │ PostgreSQL │ │
│ │ 参数: wordlist_name │─────▶│ 查询 Wordlist 表 │ │
│ │ 获取 file_path, file_hash │ │
└───────────┬─────────────┘ └─────────────────────────────────┘ │
│ │ │
▼ │
│ ┌─────────────────────────┐ │
│ │ 检查本地文件是否存在 │ │
│ │ /opt/xingrin/wordlists/ │ │
└───────────┬─────────────┘ │
│ │
┌───────┴───────┐ │
│ │ │ │
▼ ▼ │
┌────────┐ ┌────────────┐ │
│ │ 不存在 │ │ 存在 │ │
└───┬────┘ └─────┬──────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────────────────┐ │
│ 计算本地文件 SHA256 │ │
│ │ │ 与数据库 hash 比较 │ │
│ └──────────┬──────────┘ │
│ │
│ │ ┌───────┴───────┐ │
│ │ │ │ │
│ │ ▼ ▼ │
│ │ ┌──────────┐ ┌──────────────┐ │
│ │ │ 一致 │ │ 不一致 │ │
│ │ 直接使用 │ │ 需重新下载 │ │
│ │ └────┬─────┘ └───────┬──────┘ │
│ │ │ │
│ │ │ │ │
▼ │ ▼ │
┌─────────────┴─────────────────────────────────────────────────────┐ │
│ │ 从 Server API 下载 │ │
│ │ GET /api/wordlists/download/?wordlist=<name> │ │
│ │ │ │
│ │ ┌──────────┐ HTTP Request ┌──────────────────────┐ │ │
│ │ │ Worker │ ───────────────────────▶│ Server (Django) │ │ │
│ │ │◀─────────────────────── │ 返回文件内容 │ │ │
└──────────┘ File Content └──────────────────────┘ │ │
│ │ │ │
保存到: /opt/xingrin/wordlists/<filename> │ │
└───────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
┌─────────────────────────────────────────────────────────────────┐ │
│ │ 返回本地字典文件路径 │ │
│ │ /opt/xingrin/wordlists/subdomains.txt │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 执行扫描工具 │ │
│ │ puredns bruteforce -w /opt/xingrin/wordlists/xxx.txt │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
```mermaid
flowchart TB
subgraph WORKER["🔧 Worker 容器"]
direction TB
START["🎯 扫描任务<br/>需要字典"]
START --> ENSURE
ENSURE["ensure_wordlist_local()<br/>参数: wordlist_name"]
ENSURE --> QUERY
QUERY["📊 查询 PostgreSQL<br/>获取 file_path, file_hash"]
QUERY --> CHECK
CHECK{"🔍 检查本地文件<br/>/opt/xingrin/wordlists/"}
CHECK -->|不存在| DOWNLOAD
CHECK -->|存在| HASH
HASH["🔐 计算本地文件 SHA256<br/>与数据库 hash 比较"]
HASH -->|一致| USE
HASH -->|不一致| DOWNLOAD
DOWNLOAD["📥 从 Server API 下载<br/>GET /api/wordlists/download/?wordlist=name"]
DOWNLOAD --> SERVER
SERVER["🌐 HTTP Request"]
SERVER -.请求.-> API["Server (Django)<br/>返回文件内容"]
API -.响应.-> SERVER
SERVER --> SAVE
SAVE["💾 保存到本地<br/>/opt/xingrin/wordlists/filename"]
SAVE --> RETURN
USE["✅ 直接使用"] --> RETURN
RETURN["📂 返回本地字典文件路径<br/>/opt/xingrin/wordlists/subdomains.txt"]
RETURN --> EXEC
EXEC["🚀 执行扫描工具<br/>puredns bruteforce -w /opt/xingrin/wordlists/xxx.txt"]
end
style WORKER fill:#e6f3ff
style START fill:#fff4e6
style CHECK fill:#ffe6f0
style HASH fill:#ffe6f0
style USE fill:#d4edda
style DOWNLOAD fill:#f8d7da
style RETURN fill:#d4edda
style EXEC fill:#cce5ff
```
## 五、Hash 校验机制
@@ -199,25 +192,28 @@ Worker 执行扫描任务时,通过 `ensure_wordlist_local()` 获取字典:
**注意**Worker 容器只挂载了 `results``logs` 目录,没有挂载 `wordlists` 目录,所以字典文件需要通过 API 下载。
```
Worker本地/远程) Server
│ │
│ 1. 查询数据库获取 file_hash │
│─────────────────────────────────▶│
│ │
│ 2. 检查本地缓存 │
│ - 存在且 hash 匹配 → 直接使用│
│ - 不存在或不匹配 → 继续下载 │
│ │
│ 3. GET /api/wordlists/download/ │
│─────────────────────────────────▶│
│ │
4. 返回文件内容 │
│◀─────────────────────────────────│
5. 保存到本地缓存 │
/opt/xingrin/wordlists/
```mermaid
sequenceDiagram
participant W as Worker (本地/远程)
participant DB as PostgreSQL
participant S as Server API
participant FS as 本地缓存
W->>DB: 1⃣ 查询数据库获取 file_hash
DB-->>W: 返回 file_hash
W->>FS: 2⃣ 检查本地缓存
alt 存在且 hash 匹配
FS-->>W: ✅ 直接使用
else 不存在或不匹配
W->>S: 3⃣ GET /api/wordlists/download/
S-->>W: 4⃣ 返回文件内容
W->>FS: 5⃣ 保存到本地缓存<br/>/opt/xingrin/wordlists/
FS-->>W: ✅ 使用缓存文件
end
Note over W,FS: 本地 Worker 优势:<br/>• 网络延迟更低(容器内网络)<br/>• 缓存可复用(同一宿主机多次任务)
```
### 本地 Worker 的优势
@@ -235,8 +231,8 @@ Worker本地/远程) Server
WORDLISTS_PATH=/opt/xingrin/wordlists
# Server 地址Worker 用于下载文件)
PUBLIC_HOST=your-server-ip
SERVER_PORT=8888
PUBLIC_HOST=your-server-ip # 远程 Worker 会通过 https://{PUBLIC_HOST}/api 访问
SERVER_PORT=8888 # 后端容器内部端口,仅 Docker 内网监听
```
## 八、常见问题
@@ -248,8 +244,8 @@ A: 更新字典内容后会重新计算 hashWorker 下次使用时会检测
### Q: 远程 Worker 下载文件失败?
A: 检查:
1. `PUBLIC_HOST` 是否配置为 Server 的外网 IP
2. Server 端口(默认 8888是否开放
1. `PUBLIC_HOST` 是否配置为 Server 的外网 IP 或域名
2. Nginx 443 (HTTPS) 是否可达(远程 Worker 通过 nginx 访问后端)
3. Worker 到 Server 的网络是否通畅
### Q: 如何批量导入字典?

View File

@@ -233,7 +233,7 @@ show_summary() {
echo -e "${YELLOW}[!] 云服务器某些厂商默认开启了安全策略(阿里云/腾讯云/华为云等):${RESET}"
echo -e " 端口未放行可能导致无法访问或无法扫描强烈推荐用国外vps或者在云控制台放行"
echo -e " ${RESET}80, 443, 3000,8888, 5432, 6379"
echo -e " ${RESET}80, 443, 5432, 6379"
echo
}
@@ -278,6 +278,46 @@ else
success "docker compose 安装完成"
fi
# ==============================================================================
# 交换分区配置(仅 Linux
# ==============================================================================
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
# 获取当前内存大小GB四舍五入
TOTAL_MEM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}')
TOTAL_MEM_GB=$(awk "BEGIN {printf \"%.0f\", $TOTAL_MEM_KB / 1024 / 1024}")
# 获取当前交换分区大小GB四舍五入
CURRENT_SWAP_KB=$(grep SwapTotal /proc/meminfo | awk '{print $2}')
CURRENT_SWAP_GB=$(awk "BEGIN {printf \"%.0f\", $CURRENT_SWAP_KB / 1024 / 1024}")
# 推荐交换分区大小与内存相同最小1G最大8G
RECOMMENDED_SWAP=$TOTAL_MEM_GB
[ "$RECOMMENDED_SWAP" -lt 1 ] && RECOMMENDED_SWAP=1
[ "$RECOMMENDED_SWAP" -gt 8 ] && RECOMMENDED_SWAP=8
echo ""
info "系统内存: ${TOTAL_MEM_GB}GB当前交换分区: ${CURRENT_SWAP_GB}GB"
# 如果交换分区小于推荐值,提示用户
if [ "$CURRENT_SWAP_GB" -lt "$RECOMMENDED_SWAP" ]; then
echo -n -e "${BOLD}${CYAN}[?] 是否开启 ${RECOMMENDED_SWAP}GB 交换分区?可提升扫描稳定性 (Y/n) ${RESET}"
read -r setup_swap
echo
if [[ ! $setup_swap =~ ^[Nn]$ ]]; then
info "正在配置 ${RECOMMENDED_SWAP}GB 交换分区..."
if bash "$ROOT_DIR/docker/scripts/setup-swap.sh" "$RECOMMENDED_SWAP"; then
success "交换分区配置完成"
else
warn "交换分区配置失败,继续安装..."
fi
else
info "跳过交换分区配置"
fi
else
success "交换分区已足够: ${CURRENT_SWAP_GB}GB"
fi
fi
step "[3/3] 初始化配置"
DOCKER_DIR="$ROOT_DIR/docker"
if [ ! -d "$DOCKER_DIR" ]; then