wimotek-edge-agent 0.0.0-stage → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +219 -3
- package/agent.py +330 -0
- package/bin/cli.js +14 -0
- package/dist/wimotek_edge_agent-1.0.4-py3-none-any.whl +0 -0
- package/dist/wimotek_edge_agent-1.0.4.tar.gz +0 -0
- package/package.json +19 -6
- package/pyproject.toml +20 -0
- package/wimotek_edge_agent.egg-info/PKG-INFO +228 -0
- package/wimotek_edge_agent.egg-info/SOURCES.txt +9 -0
- package/wimotek_edge_agent.egg-info/dependency_links.txt +1 -0
- package/wimotek_edge_agent.egg-info/entry_points.txt +3 -0
- package/wimotek_edge_agent.egg-info/requires.txt +2 -0
- package/wimotek_edge_agent.egg-info/top_level.txt +1 -0
package/README.md
CHANGED
|
@@ -1,3 +1,219 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# Wimotek Edge Agent (wimotek-edge-agent)
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/wimotek-edge-agent/)
|
|
4
|
+
[](https://www.npmjs.com/package/wimotek-edge-agent)
|
|
5
|
+
[](https://pypi.org/project/wimotek-edge-agent/)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
轻量、通用、零侵入的边缘节点出站反向代理与服务中继客户端(Universal Edge Reverse Tunnel & Service Bridge Agent)。
|
|
9
|
+
|
|
10
|
+
专为处于 **无公网 IP、NAT4、移动宽带、家用路由器或严格云防火墙** 环境下的容器(noVNC、Kasm Workspaces、Headless Chromium、自动化爬虫等)与边缘服务器设计,提供全自动设备确权、双模身份认证与本地端口透明反向穿透能力。
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 🌟 核心特性
|
|
15
|
+
|
|
16
|
+
- **⚡ 出站长连接 NAT4 极速穿透**:通过单向发起 Outbound WebSocket (WSS/TLS) 长连接直达中心管理端,彻底突破多层 NAT 与企业内网防火墙,**节点无需公网 IP、无需路由器端口映射**。
|
|
17
|
+
- **🛡️ 生产级双模身份认证与防伪造(TOFU + Edge Agent)**:
|
|
18
|
+
- **默认 TOFU 模式(Trust On First Use)**:零配置启动,节点自动生成硬件指纹 UUID,管理端后台实时弹出审核认领,管理员「一键信任锁定」后完成公钥防篡改绑定。
|
|
19
|
+
- **Edge Agent 模式**:批量注入 `JOIN_TOKEN`,节点开机秒级自验证入网,支持 CI/CD 与大规模集群全自动挂靠。
|
|
20
|
+
- **🤖 智能命名与设备自协商**:自动探测出口公网 IP 或物理网卡 IP 智能拼接命名(如 `Node-123.45.67.89`),嗅探失败平滑降级为容器主机名。
|
|
21
|
+
- **🔌 通用本地服务透明桥接(Decoupled & Multi-Service)**:业务完全解耦,不仅支持 Chromium CDP(9222),亦可同时中继 VNC 桌面(6080)、Web 业务 API(8080)等任意本地端口。
|
|
22
|
+
- **🌐 跨语言分发与 CLI 驱动**:同时发布至 PyPI 与 npm,支持系统级全局命令 `edge-agent` 与 `npx wimotek-edge-agent`。
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 🏗️ 架构原理
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
┌────────────────────────────────────────────────────────┐
|
|
30
|
+
│ 边缘内网节点 (家用宽带 / NAT4 / 容器无公网IP) │
|
|
31
|
+
│ │
|
|
32
|
+
│ - Chromium CDP (:9222) │
|
|
33
|
+
│ - noVNC 桌面 (:6080) │
|
|
34
|
+
│ - 自定义业务服务 (:8080) │
|
|
35
|
+
└──────────────────────────┬─────────────────────────────┘
|
|
36
|
+
▲ 本地 127.0.0.1 访问
|
|
37
|
+
▼
|
|
38
|
+
┌────────────────────────────────────────────────────────┐
|
|
39
|
+
│ wimotek-edge-agent 客户端 │
|
|
40
|
+
│ │
|
|
41
|
+
│ 职责: 硬件指纹生成 + TOFU/Token协商 + 本地服务透明中继 │
|
|
42
|
+
└──────────────────────────┬─────────────────────────────┘
|
|
43
|
+
│
|
|
44
|
+
│ 主动发起出站反向长连接 (Outbound TLS/WSS)
|
|
45
|
+
│ 仅需中心管理端暴露 8443 端口,无视客户端网络环境
|
|
46
|
+
▼
|
|
47
|
+
┌────────────────────────────────────────────────────────┐
|
|
48
|
+
│ 中心控制端 (Central Hub) │
|
|
49
|
+
│ │
|
|
50
|
+
│ - 管理员在线认领锁定 (TOFU) / Token 自动化放行 │
|
|
51
|
+
│ - 借由反向长连接随时下发指令、Cookie同步、采集探针 │
|
|
52
|
+
└────────────────────────────────────────────────────────┘
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 📦 安装与调用
|
|
58
|
+
|
|
59
|
+
### 1. Python / PyPI 安装(推荐)
|
|
60
|
+
```bash
|
|
61
|
+
pip install wimotek-edge-agent
|
|
62
|
+
```
|
|
63
|
+
安装后直接获得系统全局命令 **`edge-agent`**。
|
|
64
|
+
|
|
65
|
+
### 2. Node.js / npm 免安装执行
|
|
66
|
+
```bash
|
|
67
|
+
npx wimotek-edge-agent --hub-url https://hub.yourdomain.com:8443
|
|
68
|
+
```
|
|
69
|
+
或全局安装:
|
|
70
|
+
```bash
|
|
71
|
+
npm install -g wimotek-edge-agent
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 🚀 快速上手
|
|
77
|
+
|
|
78
|
+
### 1. 命令行直接启动 (CLI)
|
|
79
|
+
```bash
|
|
80
|
+
# 最简启动 (直连模式,自动嗅探 IP 命名)
|
|
81
|
+
edge-agent --hub-url https://hub.yourdomain.com:8443
|
|
82
|
+
|
|
83
|
+
# NAT4 穿透模式 (无公网 IP 环境)
|
|
84
|
+
edge-agent --hub-url https://hub.yourdomain.com:8443 --reverse
|
|
85
|
+
|
|
86
|
+
# 批量免审批模式 (携带集群入网通行码)
|
|
87
|
+
edge-agent --hub-url https://hub.yourdomain.com:8443 --join-token hub_join_xxxxxx --reverse
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 2. 环境变量一键启动(容器化最佳实践)
|
|
91
|
+
无需修改命令,直接传入环境变量:
|
|
92
|
+
```bash
|
|
93
|
+
export HUB_URL="https://hub.yourdomain.com:8443"
|
|
94
|
+
export JOIN_TOKEN="hub_join_xxxxxx" # 可选,不填走 TOFU 待认领模式
|
|
95
|
+
export REVERSE_TUNNEL="true" # 可选,默认 false;NAT4 无公网环境设为 true
|
|
96
|
+
edge-agent
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 3. Python 脚本编程式集成
|
|
100
|
+
```python
|
|
101
|
+
from wimotek_edge_agent import EdgeAgent
|
|
102
|
+
|
|
103
|
+
agent = EdgeAgent(
|
|
104
|
+
hub_url="https://hub.yourdomain.com:8443",
|
|
105
|
+
join_token="hub_join_xxxxxx", # 可选
|
|
106
|
+
node_name="Seoul-Worker-01", # 可选,留空自动按 IP 拼接
|
|
107
|
+
node_group="爬虫矩阵组", # 默认: 默认分组
|
|
108
|
+
cdp_port=9222, # 默认: 9222
|
|
109
|
+
reverse_tunnel=True # 开启 NAT4 反向长连接
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# 启动后台代理 (阻塞运行)
|
|
113
|
+
agent.run()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## ⚙️ 环境变量与参数字典
|
|
119
|
+
|
|
120
|
+
| 环境变量名 | CLI 参数 | 默认值 | 描述 |
|
|
121
|
+
| :--- | :--- | :--- | :--- |
|
|
122
|
+
| `HUB_URL` | `--hub-url` | *无 (必填)* | 中心管理端 HTTPS/WSS 地址,如 `https://hub.domain.com:8443` |
|
|
123
|
+
| `JOIN_TOKEN` | `--join-token` | `""` (空) | 集群入网通行码。**留空走 TOFU 后台审核认领**;填入则走 Edge Agent 自动放行入网 |
|
|
124
|
+
| `REVERSE_TUNNEL`| `--reverse` / `--direct` | `false` | 是否开启出站反向长连接隧道。**无公网 IP/NAT4 环境设为 `true`**;有公网/局域网设为 `false` |
|
|
125
|
+
| `NODE_NAME` | `--node-name` | 自动嗅探 | 节点名称。未设置时自动以出口 IP 命名(如 `Node-123.45.67.89`),兜底为主机名 |
|
|
126
|
+
| `NODE_GROUP` | `--group` | `默认分组` | 节点所属集群分组,便于多策略调度分类 |
|
|
127
|
+
| `CDP_PORT` | `--port` | `9222` | 容器本地需要中继的主服务端口(如 Chromium 调试端口) |
|
|
128
|
+
| `LOCAL_CDP_HOST`| `--cdp-host` | `127.0.0.1` | 节点本地被中继服务的绑定地址 |
|
|
129
|
+
| `SERVICES` | `--services` | *无* | 多服务端口定义,格式:`cdp:9222,vnc:6080,api:8080` |
|
|
130
|
+
| `UUID_FILE` | `--uuid-file` | `/data/node_machine.id` | 硬件指纹持久化文件路径,保障重启后身份不漂移 |
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 🐳 Docker 生产环境集成示例
|
|
135
|
+
|
|
136
|
+
### 场景 1:noVNC / Kasm 节点 Dockerfile 集成
|
|
137
|
+
在现有的浏览器镜像中,只需增加 `pip install` 即可:
|
|
138
|
+
|
|
139
|
+
```dockerfile
|
|
140
|
+
FROM debian:bookworm-slim
|
|
141
|
+
|
|
142
|
+
# 安装 Chromium、noVNC 及基础环境
|
|
143
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
144
|
+
chromium xvfb x11vnc novnc websockify python3 python3-pip curl \
|
|
145
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
146
|
+
|
|
147
|
+
# 一行安装通用边缘 Agent
|
|
148
|
+
RUN pip3 install --no-cache-dir --break-system-packages wimotek-edge-agent
|
|
149
|
+
|
|
150
|
+
COPY entrypoint.sh /app/entrypoint.sh
|
|
151
|
+
RUN chmod +x /app/entrypoint.sh
|
|
152
|
+
ENTRYPOINT ["/app/entrypoint.sh"]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
在 `entrypoint.sh` 中只需一行后台唤起:
|
|
156
|
+
```bash
|
|
157
|
+
#!/bin/bash
|
|
158
|
+
# 启动本地服务 (如 Chromium 9222)
|
|
159
|
+
chromium --remote-debugging-port=9222 --remote-allow-origins=* &
|
|
160
|
+
|
|
161
|
+
# 只要注入了 HUB_URL,自动在后台拉起边缘代理
|
|
162
|
+
if [ -n "$HUB_URL" ]; then
|
|
163
|
+
echo "🚀 启动 wimotek-edge-agent ..."
|
|
164
|
+
edge-agent &
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
wait -n
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 场景 2:`docker-compose.yml` 极简部署
|
|
171
|
+
|
|
172
|
+
#### 无公网 IP / 家用宽带 / NAT4(开启反向长连接):
|
|
173
|
+
```yaml
|
|
174
|
+
version: '3.8'
|
|
175
|
+
|
|
176
|
+
services:
|
|
177
|
+
browser-node:
|
|
178
|
+
image: your-browser-image:latest
|
|
179
|
+
restart: unless-stopped
|
|
180
|
+
ports:
|
|
181
|
+
- "6080:6080" # 仅供内网访问桌面,公网无需暴露 9222
|
|
182
|
+
environment:
|
|
183
|
+
- HUB_URL=https://hub.yourdomain.com:8443
|
|
184
|
+
- JOIN_TOKEN=hub_join_abcdef1234567890 # 可选:留空在控制台一键点击认领
|
|
185
|
+
- REVERSE_TUNNEL=true # NAT4 穿透开关
|
|
186
|
+
- NODE_GROUP=北美爬虫矩阵组
|
|
187
|
+
volumes:
|
|
188
|
+
- ./data:/data
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### 拥有独立公网 IP 的云服务器(直连心跳上报):
|
|
192
|
+
```yaml
|
|
193
|
+
version: '3.8'
|
|
194
|
+
|
|
195
|
+
services:
|
|
196
|
+
browser-node:
|
|
197
|
+
image: your-browser-image:latest
|
|
198
|
+
restart: unless-stopped
|
|
199
|
+
ports:
|
|
200
|
+
- "9222:9222" # 防火墙已开放端口
|
|
201
|
+
environment:
|
|
202
|
+
- HUB_URL=https://hub.yourdomain.com:8443
|
|
203
|
+
- REVERSE_TUNNEL=false # 默认即为 false,仅发起轻量 HTTP 登记,零长连接开销
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## 🔒 安全性考量
|
|
209
|
+
|
|
210
|
+
1. **防恶意伪造(SSRF 防御)**:在直连上报模式下,管理端强制从底层 TCP 握手来源直接锁定节点的真实来源 IP,杜绝假冒内网私网地址。
|
|
211
|
+
2. **防公网扫描冒充**:未携带 `JOIN_TOKEN` 的未知连接一律进入隔离待审核队列(TOFU),未经管理端管理员后台显式认领的节点,绝不参与任何数据交互。
|
|
212
|
+
3. **安全通信**:长连接反向通道强制基于 TLS (WSS) 传输,天然防御中间人窃听与数据篡改。
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## 📄 License
|
|
217
|
+
|
|
218
|
+
MIT License © 2026 [Wimotek](https://github.com/wimotek)
|
|
219
|
+
```
|
package/agent.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Wimotek Universal Edge Agent
|
|
4
|
+
A lightweight, clean reverse tunnel & service bridge client.
|
|
5
|
+
|
|
6
|
+
State File Logic:
|
|
7
|
+
- User specified path (param / UUID_FILE env) > Default (/data/node_machine.id)
|
|
8
|
+
- If permission denied, logs clear error instructions for the user to resolve.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
import json
|
|
15
|
+
import uuid
|
|
16
|
+
import socket
|
|
17
|
+
import asyncio
|
|
18
|
+
import argparse
|
|
19
|
+
import urllib.parse
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Dict, Optional, Callable
|
|
22
|
+
import httpx
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def resolve_state_file(filename: str = "node_machine.id", custom_path: Optional[str] = None) -> Path:
|
|
26
|
+
"""
|
|
27
|
+
极简文件路径解析:
|
|
28
|
+
1. 显式入参 / 环境变量 UUID_FILE
|
|
29
|
+
2. 默认 /data/<filename>
|
|
30
|
+
"""
|
|
31
|
+
if custom_path:
|
|
32
|
+
return Path(custom_path).expanduser().resolve()
|
|
33
|
+
|
|
34
|
+
env_override = os.getenv("UUID_FILE") or os.getenv("STATE_FILE")
|
|
35
|
+
if env_override:
|
|
36
|
+
return Path(env_override).expanduser().resolve()
|
|
37
|
+
|
|
38
|
+
return Path("/data") / filename
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class EdgeAgent:
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
hub_url: Optional[str] = None,
|
|
45
|
+
join_token: Optional[str] = None,
|
|
46
|
+
node_name: Optional[str] = None,
|
|
47
|
+
node_group: Optional[str] = None,
|
|
48
|
+
target_port: Optional[int] = None,
|
|
49
|
+
target_host: Optional[str] = None,
|
|
50
|
+
services: Optional[Dict[str, int]] = None,
|
|
51
|
+
reverse_tunnel: Optional[bool] = None,
|
|
52
|
+
uuid_file: Optional[str] = None,
|
|
53
|
+
):
|
|
54
|
+
self.hub_url = (hub_url or os.getenv("HUB_URL", "")).rstrip("/")
|
|
55
|
+
self.join_token = (join_token or os.getenv("JOIN_TOKEN", "")).strip()
|
|
56
|
+
self.node_group = (node_group or os.getenv("NODE_GROUP", "默认分组")).strip()
|
|
57
|
+
self.target_host = (target_host or os.getenv("TARGET_HOST") or os.getenv("LOCAL_HOST") or "127.0.0.1").strip()
|
|
58
|
+
|
|
59
|
+
# 端口默认 9222
|
|
60
|
+
raw_port = target_port if target_port is not None else (os.getenv("TARGET_PORT") or os.getenv("CDP_PORT") or "9222")
|
|
61
|
+
try:
|
|
62
|
+
self.target_port = int(raw_port)
|
|
63
|
+
except ValueError:
|
|
64
|
+
self.target_port = 9222
|
|
65
|
+
|
|
66
|
+
self.services: Dict[str, int] = services or self._parse_services()
|
|
67
|
+
self.services["default"] = self.target_port
|
|
68
|
+
|
|
69
|
+
# 默认 false (直连上报),显式为 true 时走反向隧道
|
|
70
|
+
if reverse_tunnel is not None:
|
|
71
|
+
self.reverse_tunnel = bool(reverse_tunnel)
|
|
72
|
+
else:
|
|
73
|
+
self.reverse_tunnel = os.getenv("REVERSE_TUNNEL", "false").lower() in ["true", "1", "yes"]
|
|
74
|
+
|
|
75
|
+
# 节点名默认按出口 IP 或主机名拼接
|
|
76
|
+
env_name = (node_name or os.getenv("NODE_NAME", "")).strip()
|
|
77
|
+
self.node_name = env_name if env_name else self._auto_detect_name()
|
|
78
|
+
|
|
79
|
+
# 极简指纹路径定位:显式指定 > /data/node_machine.id
|
|
80
|
+
self.uuid_path = resolve_state_file("node_machine.id", custom_path=uuid_file)
|
|
81
|
+
self.node_uuid = self._load_or_create_uuid()
|
|
82
|
+
|
|
83
|
+
# 通信协议端点解析
|
|
84
|
+
self.http_base = ""
|
|
85
|
+
self.ws_base = ""
|
|
86
|
+
if self.hub_url:
|
|
87
|
+
parsed = urllib.parse.urlparse(self.hub_url)
|
|
88
|
+
self.http_base = f"{parsed.scheme}://{parsed.netloc}"
|
|
89
|
+
ws_scheme = "wss" if parsed.scheme == "https" else "ws"
|
|
90
|
+
self.ws_base = f"{ws_scheme}://{parsed.netloc}"
|
|
91
|
+
|
|
92
|
+
self._action_handlers: Dict[str, Callable] = {}
|
|
93
|
+
|
|
94
|
+
def register_action(self, action_name: str, handler: Callable):
|
|
95
|
+
"""注册自定义业务动作"""
|
|
96
|
+
self._action_handlers[action_name] = handler
|
|
97
|
+
|
|
98
|
+
def _parse_services(self) -> Dict[str, int]:
|
|
99
|
+
services = {}
|
|
100
|
+
raw = os.getenv("SERVICES", "").strip()
|
|
101
|
+
if raw:
|
|
102
|
+
for item in raw.split(","):
|
|
103
|
+
if ":" in item:
|
|
104
|
+
name, p = item.split(":", 1)
|
|
105
|
+
try:
|
|
106
|
+
services[name.strip()] = int(p.strip())
|
|
107
|
+
except ValueError:
|
|
108
|
+
pass
|
|
109
|
+
return services
|
|
110
|
+
|
|
111
|
+
def _auto_detect_name(self) -> str:
|
|
112
|
+
try:
|
|
113
|
+
with httpx.Client(timeout=1.5) as client:
|
|
114
|
+
res = client.get("https://api.ipify.org")
|
|
115
|
+
if res.status_code == 200 and res.text.strip():
|
|
116
|
+
return f"Node-{res.text.strip()}"
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
122
|
+
s.connect(("8.8.8.8", 80))
|
|
123
|
+
ip = s.getsockname()[0]
|
|
124
|
+
s.close()
|
|
125
|
+
if ip and ip != "127.0.0.1":
|
|
126
|
+
return f"Node-{ip}"
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
return f"Node-{socket.gethostname()[:12]}"
|
|
131
|
+
|
|
132
|
+
def _load_or_create_uuid(self) -> str:
|
|
133
|
+
"""读取或创建指纹,遇无权限/目录不存在直接清晰报错输出指导"""
|
|
134
|
+
# 1. 尝试读取已有文件
|
|
135
|
+
if self.uuid_path.is_file():
|
|
136
|
+
try:
|
|
137
|
+
content = self.uuid_path.read_text().strip()
|
|
138
|
+
if content:
|
|
139
|
+
return content
|
|
140
|
+
except Exception as e:
|
|
141
|
+
print(f"⚠️ [指纹读取失败] 无法读取已有指纹文件 ({self.uuid_path}): {e}")
|
|
142
|
+
|
|
143
|
+
# 2. 首次写入持久化
|
|
144
|
+
new_id = str(uuid.uuid4())
|
|
145
|
+
try:
|
|
146
|
+
self.uuid_path.parent.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
self.uuid_path.write_text(new_id)
|
|
148
|
+
return new_id
|
|
149
|
+
except PermissionError:
|
|
150
|
+
print(f"❌ [权限错误] 无权限写入指纹文件: {self.uuid_path}")
|
|
151
|
+
print(f"👉 解决方案:请检查容器挂载目录写权限,或指定环境变量 UUID_FILE 改为可写路径,例如: -e UUID_FILE=/my_path/{self.uuid_path.name}")
|
|
152
|
+
except Exception as e:
|
|
153
|
+
print(f"❌ [目录错误] 无法保存指纹文件 ({self.uuid_path}): {e}")
|
|
154
|
+
print(f"👉 解决方案:请确认目录是否存在并具备写权限,或通过环境变量 UUID_FILE 指定有效路径。")
|
|
155
|
+
|
|
156
|
+
# 仅在严重报错时使用内存 UUID,避免脚本崩溃打断主业务
|
|
157
|
+
return new_id
|
|
158
|
+
|
|
159
|
+
def run_direct_report(self):
|
|
160
|
+
"""直连上报模式:轻量 HTTP 登记 + 心跳"""
|
|
161
|
+
print(f"📡 [Direct-Mode] 正在向主控中心登记: {self.http_base} ...")
|
|
162
|
+
headers = {"Content-Type": "application/json"}
|
|
163
|
+
if self.join_token:
|
|
164
|
+
headers["X-Join-Token"] = self.join_token
|
|
165
|
+
|
|
166
|
+
payload = {
|
|
167
|
+
"node_uuid": self.node_uuid,
|
|
168
|
+
"name": self.node_name,
|
|
169
|
+
"group_name": self.node_group,
|
|
170
|
+
"cdp_port": self.target_port,
|
|
171
|
+
"services": self.services,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
while True:
|
|
175
|
+
try:
|
|
176
|
+
with httpx.Client(timeout=10.0, verify=False) as client:
|
|
177
|
+
res = client.post(f"{self.http_base}/api/nodes/auto-register", json=payload, headers=headers)
|
|
178
|
+
print(f"✅ [{time.strftime('%X')}] 节点上报/心跳成功: {res.text}")
|
|
179
|
+
except Exception as e:
|
|
180
|
+
print(f"⚠️ [{time.strftime('%X')}] 上报失败,60秒后重试: {e}")
|
|
181
|
+
time.sleep(60)
|
|
182
|
+
|
|
183
|
+
async def run_reverse_tunnel(self):
|
|
184
|
+
"""反向长连接隧道模式:全双工出站连接穿透 NAT4"""
|
|
185
|
+
import websockets
|
|
186
|
+
|
|
187
|
+
params = urllib.parse.urlencode({
|
|
188
|
+
"uuid": self.node_uuid,
|
|
189
|
+
"name": self.node_name,
|
|
190
|
+
"group": self.node_group,
|
|
191
|
+
"token": self.join_token,
|
|
192
|
+
"port": self.target_port,
|
|
193
|
+
})
|
|
194
|
+
ws_endpoint = f"{self.ws_base}/api/nodes/reverse-tunnel?{params}"
|
|
195
|
+
is_ssl = self.ws_base.startswith("wss://")
|
|
196
|
+
|
|
197
|
+
while True:
|
|
198
|
+
try:
|
|
199
|
+
print(f"📡 [Tunnel-Mode] 正在建立反向长连接 -> {self.hub_url} ...")
|
|
200
|
+
async with websockets.connect(ws_endpoint, timeout=10.0, ssl=False if not is_ssl else None) as ws:
|
|
201
|
+
print(f"✅ [Tunnel-Connected] 隧道就绪!指纹存储于: {self.uuid_path}")
|
|
202
|
+
|
|
203
|
+
while True:
|
|
204
|
+
msg_text = await ws.recv()
|
|
205
|
+
req = json.loads(msg_text)
|
|
206
|
+
req_id = req.get("req_id")
|
|
207
|
+
action = req.get("action", "http_proxy")
|
|
208
|
+
payload = req.get("payload", {})
|
|
209
|
+
|
|
210
|
+
# 1. 自定义扩展指令
|
|
211
|
+
if action in self._action_handlers:
|
|
212
|
+
try:
|
|
213
|
+
handler = self._action_handlers[action]
|
|
214
|
+
if asyncio.iscoroutinefunction(handler):
|
|
215
|
+
resp_data = await handler(payload)
|
|
216
|
+
else:
|
|
217
|
+
resp_data = handler(payload)
|
|
218
|
+
await ws.send(json.dumps({"req_id": req_id, "data": resp_data}))
|
|
219
|
+
except Exception as ex:
|
|
220
|
+
await ws.send(json.dumps({"req_id": req_id, "error": str(ex)}))
|
|
221
|
+
|
|
222
|
+
# 2. 通用透明 HTTP 反向中继
|
|
223
|
+
elif action in ["http_proxy", "service_proxy"]:
|
|
224
|
+
port = payload.get("port") or self.services.get(payload.get("service", "default"), self.target_port)
|
|
225
|
+
path = payload.get("path", "/")
|
|
226
|
+
method = payload.get("method", "GET")
|
|
227
|
+
body = payload.get("body")
|
|
228
|
+
headers = payload.get("headers", {})
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
232
|
+
resp = await client.request(
|
|
233
|
+
method=method,
|
|
234
|
+
url=f"http://{self.target_host}:{port}{path}",
|
|
235
|
+
headers=headers,
|
|
236
|
+
content=body,
|
|
237
|
+
)
|
|
238
|
+
await ws.send(json.dumps({
|
|
239
|
+
"req_id": req_id,
|
|
240
|
+
"status_code": resp.status_code,
|
|
241
|
+
"headers": dict(resp.headers),
|
|
242
|
+
"body": resp.text,
|
|
243
|
+
}))
|
|
244
|
+
except Exception as ex:
|
|
245
|
+
await ws.send(json.dumps({
|
|
246
|
+
"req_id": req_id,
|
|
247
|
+
"status_code": 502,
|
|
248
|
+
"error": str(ex),
|
|
249
|
+
}))
|
|
250
|
+
|
|
251
|
+
# 3. 通用透明 WebSocket 管道转发
|
|
252
|
+
elif action == "ws_proxy":
|
|
253
|
+
port = payload.get("port") or self.services.get("default", self.target_port)
|
|
254
|
+
ws_path = payload.get("path", "")
|
|
255
|
+
local_ws_url = f"ws://{self.target_host}:{port}{ws_path}"
|
|
256
|
+
send_data = payload.get("data")
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
async with websockets.connect(local_ws_url, timeout=5.0) as local_ws:
|
|
260
|
+
if send_data:
|
|
261
|
+
await local_ws.send(send_data)
|
|
262
|
+
reply = await local_ws.recv()
|
|
263
|
+
await ws.send(json.dumps({
|
|
264
|
+
"req_id": req_id,
|
|
265
|
+
"success": True,
|
|
266
|
+
"data": reply,
|
|
267
|
+
}))
|
|
268
|
+
except Exception as ex:
|
|
269
|
+
await ws.send(json.dumps({
|
|
270
|
+
"req_id": req_id,
|
|
271
|
+
"success": False,
|
|
272
|
+
"error": str(ex),
|
|
273
|
+
}))
|
|
274
|
+
|
|
275
|
+
# 4. 心跳响应
|
|
276
|
+
elif action == "ping":
|
|
277
|
+
await ws.send(json.dumps({"req_id": req_id, "action": "pong", "time": time.time()}))
|
|
278
|
+
|
|
279
|
+
except (websockets.ConnectionClosed, Exception) as e:
|
|
280
|
+
print(f"⚠️ 反向连接断开,5秒后自动重连: {e}")
|
|
281
|
+
await asyncio.sleep(5)
|
|
282
|
+
|
|
283
|
+
def run(self):
|
|
284
|
+
"""运行入口"""
|
|
285
|
+
if not self.hub_url:
|
|
286
|
+
print("ℹ️ 未配置 HUB_URL,Agent 停止运行。")
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
print(f"🚀 [Edge-Agent] 启动节点: {self.node_name} | 模式: {'反向穿透(NAT4)' if self.reverse_tunnel else '直连上报'} | 主端口: {self.target_port} | 指纹位置: {self.uuid_path}")
|
|
290
|
+
if self.reverse_tunnel:
|
|
291
|
+
asyncio.run(self.run_reverse_tunnel())
|
|
292
|
+
else:
|
|
293
|
+
self.run_direct_report()
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def main():
|
|
297
|
+
parser = argparse.ArgumentParser(description="Wimotek Universal Edge Agent")
|
|
298
|
+
parser.add_argument("--hub-url", type=str, help="中心管理端 URL (如 https://hub.domain.com:8443)")
|
|
299
|
+
parser.add_argument("--join-token", type=str, help="入网认证 Token (可选)")
|
|
300
|
+
parser.add_argument("--node-name", type=str, help="节点名称 (默认自动嗅探 IP)")
|
|
301
|
+
parser.add_argument("--group", type=str, help="所属集群分组")
|
|
302
|
+
parser.add_argument("--port", type=int, help="要代理的主端口 (默认: 9222)")
|
|
303
|
+
parser.add_argument("--host", type=str, help="本地服务绑定地址 (默认: 127.0.0.1)")
|
|
304
|
+
parser.add_argument("--services", type=str, help="多端口代理定义, 如: cdp:9222,vnc:6080")
|
|
305
|
+
parser.add_argument("--state-file", type=str, help="指纹文件存储路径 (默认: /data/node_machine.id)")
|
|
306
|
+
parser.add_argument("--reverse", action="store_true", help="强制开启出站反向长连接隧道")
|
|
307
|
+
parser.add_argument("--direct", action="store_true", help="强制开启直连上报心跳模式")
|
|
308
|
+
args = parser.parse_args()
|
|
309
|
+
|
|
310
|
+
rev_mode = None
|
|
311
|
+
if args.reverse:
|
|
312
|
+
rev_mode = True
|
|
313
|
+
elif args.direct:
|
|
314
|
+
rev_mode = False
|
|
315
|
+
|
|
316
|
+
agent = EdgeAgent(
|
|
317
|
+
hub_url=args.hub_url,
|
|
318
|
+
join_token=args.join_token,
|
|
319
|
+
node_name=args.node_name,
|
|
320
|
+
node_group=args.group,
|
|
321
|
+
target_port=args.port,
|
|
322
|
+
target_host=args.host,
|
|
323
|
+
reverse_tunnel=rev_mode,
|
|
324
|
+
uuid_file=args.state_file,
|
|
325
|
+
)
|
|
326
|
+
agent.run()
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
if __name__ == "__main__":
|
|
330
|
+
main()
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { spawn } = require('child_process');
|
|
3
|
+
|
|
4
|
+
const args = ['-m', 'wimotek_edge_agent.agent', ...process.argv.slice(2)];
|
|
5
|
+
const py = spawn('python3', args, { stdio: 'inherit', env: process.env });
|
|
6
|
+
|
|
7
|
+
py.on('error', () => {
|
|
8
|
+
console.error('❌ 请确保宿主机已安装 Python 3 和 wimotek-edge-agent: pip install wimotek-edge-agent');
|
|
9
|
+
process.exit(1);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
py.on('close', (code) => {
|
|
13
|
+
process.exit(code);
|
|
14
|
+
});
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "wimotek-edge-agent",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "wimotek-edge-agent",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "Universal Edge Tunnel Agent CLI Runner",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"edge-agent": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"edge",
|
|
11
|
+
"agent",
|
|
12
|
+
"tunnel",
|
|
13
|
+
"reverse-proxy",
|
|
14
|
+
"cdp",
|
|
15
|
+
"vnc"
|
|
16
|
+
],
|
|
17
|
+
"author": "wimotek",
|
|
18
|
+
"license": "MIT"
|
|
19
|
+
}
|
package/pyproject.toml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"setuptools>=61.0",
|
|
4
|
+
]
|
|
5
|
+
build-backend = "setuptools.build_meta"
|
|
6
|
+
|
|
7
|
+
[project]
|
|
8
|
+
name = "wimotek-edge-agent"
|
|
9
|
+
version = "1.0.4"
|
|
10
|
+
description = "Universal Edge Reverse Proxy & Service Bridge Agent"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"httpx>=0.24.0",
|
|
15
|
+
"websockets>=11.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
edge-agent = "wimotek_edge_agent.agent:main"
|
|
20
|
+
wimotek-agent = "wimotek_edge_agent.agent:main"
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wimotek-edge-agent
|
|
3
|
+
Version: 1.0.4
|
|
4
|
+
Summary: Universal Edge Reverse Proxy & Service Bridge Agent
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: httpx>=0.24.0
|
|
8
|
+
Requires-Dist: websockets>=11.0
|
|
9
|
+
|
|
10
|
+
# Wimotek Edge Agent (wimotek-edge-agent)
|
|
11
|
+
|
|
12
|
+
[](https://pypi.org/project/wimotek-edge-agent/)
|
|
13
|
+
[](https://www.npmjs.com/package/wimotek-edge-agent)
|
|
14
|
+
[](https://pypi.org/project/wimotek-edge-agent/)
|
|
15
|
+
[](https://opensource.org/licenses/MIT)
|
|
16
|
+
|
|
17
|
+
轻量、通用、零侵入的边缘节点出站反向代理与服务中继客户端(Universal Edge Reverse Tunnel & Service Bridge Agent)。
|
|
18
|
+
|
|
19
|
+
专为处于 **无公网 IP、NAT4、移动宽带、家用路由器或严格云防火墙** 环境下的容器(noVNC、Kasm Workspaces、Headless Chromium、自动化爬虫等)与边缘服务器设计,提供全自动设备确权、双模身份认证与本地端口透明反向穿透能力。
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 🌟 核心特性
|
|
24
|
+
|
|
25
|
+
- **⚡ 出站长连接 NAT4 极速穿透**:通过单向发起 Outbound WebSocket (WSS/TLS) 长连接直达中心管理端,彻底突破多层 NAT 与企业内网防火墙,**节点无需公网 IP、无需路由器端口映射**。
|
|
26
|
+
- **🛡️ 生产级双模身份认证与防伪造(TOFU + Edge Agent)**:
|
|
27
|
+
- **默认 TOFU 模式(Trust On First Use)**:零配置启动,节点自动生成硬件指纹 UUID,管理端后台实时弹出审核认领,管理员「一键信任锁定」后完成公钥防篡改绑定。
|
|
28
|
+
- **Edge Agent 模式**:批量注入 `JOIN_TOKEN`,节点开机秒级自验证入网,支持 CI/CD 与大规模集群全自动挂靠。
|
|
29
|
+
- **🤖 智能命名与设备自协商**:自动探测出口公网 IP 或物理网卡 IP 智能拼接命名(如 `Node-123.45.67.89`),嗅探失败平滑降级为容器主机名。
|
|
30
|
+
- **🔌 通用本地服务透明桥接(Decoupled & Multi-Service)**:业务完全解耦,不仅支持 Chromium CDP(9222),亦可同时中继 VNC 桌面(6080)、Web 业务 API(8080)等任意本地端口。
|
|
31
|
+
- **🌐 跨语言分发与 CLI 驱动**:同时发布至 PyPI 与 npm,支持系统级全局命令 `edge-agent` 与 `npx wimotek-edge-agent`。
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 🏗️ 架构原理
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
┌────────────────────────────────────────────────────────┐
|
|
39
|
+
│ 边缘内网节点 (家用宽带 / NAT4 / 容器无公网IP) │
|
|
40
|
+
│ │
|
|
41
|
+
│ - Chromium CDP (:9222) │
|
|
42
|
+
│ - noVNC 桌面 (:6080) │
|
|
43
|
+
│ - 自定义业务服务 (:8080) │
|
|
44
|
+
└──────────────────────────┬─────────────────────────────┘
|
|
45
|
+
▲ 本地 127.0.0.1 访问
|
|
46
|
+
▼
|
|
47
|
+
┌────────────────────────────────────────────────────────┐
|
|
48
|
+
│ wimotek-edge-agent 客户端 │
|
|
49
|
+
│ │
|
|
50
|
+
│ 职责: 硬件指纹生成 + TOFU/Token协商 + 本地服务透明中继 │
|
|
51
|
+
└──────────────────────────┬─────────────────────────────┘
|
|
52
|
+
│
|
|
53
|
+
│ 主动发起出站反向长连接 (Outbound TLS/WSS)
|
|
54
|
+
│ 仅需中心管理端暴露 8443 端口,无视客户端网络环境
|
|
55
|
+
▼
|
|
56
|
+
┌────────────────────────────────────────────────────────┐
|
|
57
|
+
│ 中心控制端 (Central Hub) │
|
|
58
|
+
│ │
|
|
59
|
+
│ - 管理员在线认领锁定 (TOFU) / Token 自动化放行 │
|
|
60
|
+
│ - 借由反向长连接随时下发指令、Cookie同步、采集探针 │
|
|
61
|
+
└────────────────────────────────────────────────────────┘
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 📦 安装与调用
|
|
67
|
+
|
|
68
|
+
### 1. Python / PyPI 安装(推荐)
|
|
69
|
+
```bash
|
|
70
|
+
pip install wimotek-edge-agent
|
|
71
|
+
```
|
|
72
|
+
安装后直接获得系统全局命令 **`edge-agent`**。
|
|
73
|
+
|
|
74
|
+
### 2. Node.js / npm 免安装执行
|
|
75
|
+
```bash
|
|
76
|
+
npx wimotek-edge-agent --hub-url https://hub.yourdomain.com:8443
|
|
77
|
+
```
|
|
78
|
+
或全局安装:
|
|
79
|
+
```bash
|
|
80
|
+
npm install -g wimotek-edge-agent
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 🚀 快速上手
|
|
86
|
+
|
|
87
|
+
### 1. 命令行直接启动 (CLI)
|
|
88
|
+
```bash
|
|
89
|
+
# 最简启动 (直连模式,自动嗅探 IP 命名)
|
|
90
|
+
edge-agent --hub-url https://hub.yourdomain.com:8443
|
|
91
|
+
|
|
92
|
+
# NAT4 穿透模式 (无公网 IP 环境)
|
|
93
|
+
edge-agent --hub-url https://hub.yourdomain.com:8443 --reverse
|
|
94
|
+
|
|
95
|
+
# 批量免审批模式 (携带集群入网通行码)
|
|
96
|
+
edge-agent --hub-url https://hub.yourdomain.com:8443 --join-token hub_join_xxxxxx --reverse
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 2. 环境变量一键启动(容器化最佳实践)
|
|
100
|
+
无需修改命令,直接传入环境变量:
|
|
101
|
+
```bash
|
|
102
|
+
export HUB_URL="https://hub.yourdomain.com:8443"
|
|
103
|
+
export JOIN_TOKEN="hub_join_xxxxxx" # 可选,不填走 TOFU 待认领模式
|
|
104
|
+
export REVERSE_TUNNEL="true" # 可选,默认 false;NAT4 无公网环境设为 true
|
|
105
|
+
edge-agent
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 3. Python 脚本编程式集成
|
|
109
|
+
```python
|
|
110
|
+
from wimotek_edge_agent import EdgeAgent
|
|
111
|
+
|
|
112
|
+
agent = EdgeAgent(
|
|
113
|
+
hub_url="https://hub.yourdomain.com:8443",
|
|
114
|
+
join_token="hub_join_xxxxxx", # 可选
|
|
115
|
+
node_name="Seoul-Worker-01", # 可选,留空自动按 IP 拼接
|
|
116
|
+
node_group="爬虫矩阵组", # 默认: 默认分组
|
|
117
|
+
cdp_port=9222, # 默认: 9222
|
|
118
|
+
reverse_tunnel=True # 开启 NAT4 反向长连接
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# 启动后台代理 (阻塞运行)
|
|
122
|
+
agent.run()
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## ⚙️ 环境变量与参数字典
|
|
128
|
+
|
|
129
|
+
| 环境变量名 | CLI 参数 | 默认值 | 描述 |
|
|
130
|
+
| :--- | :--- | :--- | :--- |
|
|
131
|
+
| `HUB_URL` | `--hub-url` | *无 (必填)* | 中心管理端 HTTPS/WSS 地址,如 `https://hub.domain.com:8443` |
|
|
132
|
+
| `JOIN_TOKEN` | `--join-token` | `""` (空) | 集群入网通行码。**留空走 TOFU 后台审核认领**;填入则走 Edge Agent 自动放行入网 |
|
|
133
|
+
| `REVERSE_TUNNEL`| `--reverse` / `--direct` | `false` | 是否开启出站反向长连接隧道。**无公网 IP/NAT4 环境设为 `true`**;有公网/局域网设为 `false` |
|
|
134
|
+
| `NODE_NAME` | `--node-name` | 自动嗅探 | 节点名称。未设置时自动以出口 IP 命名(如 `Node-123.45.67.89`),兜底为主机名 |
|
|
135
|
+
| `NODE_GROUP` | `--group` | `默认分组` | 节点所属集群分组,便于多策略调度分类 |
|
|
136
|
+
| `CDP_PORT` | `--port` | `9222` | 容器本地需要中继的主服务端口(如 Chromium 调试端口) |
|
|
137
|
+
| `LOCAL_CDP_HOST`| `--cdp-host` | `127.0.0.1` | 节点本地被中继服务的绑定地址 |
|
|
138
|
+
| `SERVICES` | `--services` | *无* | 多服务端口定义,格式:`cdp:9222,vnc:6080,api:8080` |
|
|
139
|
+
| `UUID_FILE` | `--uuid-file` | `/data/node_machine.id` | 硬件指纹持久化文件路径,保障重启后身份不漂移 |
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 🐳 Docker 生产环境集成示例
|
|
144
|
+
|
|
145
|
+
### 场景 1:noVNC / Kasm 节点 Dockerfile 集成
|
|
146
|
+
在现有的浏览器镜像中,只需增加 `pip install` 即可:
|
|
147
|
+
|
|
148
|
+
```dockerfile
|
|
149
|
+
FROM debian:bookworm-slim
|
|
150
|
+
|
|
151
|
+
# 安装 Chromium、noVNC 及基础环境
|
|
152
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
153
|
+
chromium xvfb x11vnc novnc websockify python3 python3-pip curl \
|
|
154
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
155
|
+
|
|
156
|
+
# 一行安装通用边缘 Agent
|
|
157
|
+
RUN pip3 install --no-cache-dir --break-system-packages wimotek-edge-agent
|
|
158
|
+
|
|
159
|
+
COPY entrypoint.sh /app/entrypoint.sh
|
|
160
|
+
RUN chmod +x /app/entrypoint.sh
|
|
161
|
+
ENTRYPOINT ["/app/entrypoint.sh"]
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
在 `entrypoint.sh` 中只需一行后台唤起:
|
|
165
|
+
```bash
|
|
166
|
+
#!/bin/bash
|
|
167
|
+
# 启动本地服务 (如 Chromium 9222)
|
|
168
|
+
chromium --remote-debugging-port=9222 --remote-allow-origins=* &
|
|
169
|
+
|
|
170
|
+
# 只要注入了 HUB_URL,自动在后台拉起边缘代理
|
|
171
|
+
if [ -n "$HUB_URL" ]; then
|
|
172
|
+
echo "🚀 启动 wimotek-edge-agent ..."
|
|
173
|
+
edge-agent &
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
wait -n
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 场景 2:`docker-compose.yml` 极简部署
|
|
180
|
+
|
|
181
|
+
#### 无公网 IP / 家用宽带 / NAT4(开启反向长连接):
|
|
182
|
+
```yaml
|
|
183
|
+
version: '3.8'
|
|
184
|
+
|
|
185
|
+
services:
|
|
186
|
+
browser-node:
|
|
187
|
+
image: your-browser-image:latest
|
|
188
|
+
restart: unless-stopped
|
|
189
|
+
ports:
|
|
190
|
+
- "6080:6080" # 仅供内网访问桌面,公网无需暴露 9222
|
|
191
|
+
environment:
|
|
192
|
+
- HUB_URL=https://hub.yourdomain.com:8443
|
|
193
|
+
- JOIN_TOKEN=hub_join_abcdef1234567890 # 可选:留空在控制台一键点击认领
|
|
194
|
+
- REVERSE_TUNNEL=true # NAT4 穿透开关
|
|
195
|
+
- NODE_GROUP=北美爬虫矩阵组
|
|
196
|
+
volumes:
|
|
197
|
+
- ./data:/data
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
#### 拥有独立公网 IP 的云服务器(直连心跳上报):
|
|
201
|
+
```yaml
|
|
202
|
+
version: '3.8'
|
|
203
|
+
|
|
204
|
+
services:
|
|
205
|
+
browser-node:
|
|
206
|
+
image: your-browser-image:latest
|
|
207
|
+
restart: unless-stopped
|
|
208
|
+
ports:
|
|
209
|
+
- "9222:9222" # 防火墙已开放端口
|
|
210
|
+
environment:
|
|
211
|
+
- HUB_URL=https://hub.yourdomain.com:8443
|
|
212
|
+
- REVERSE_TUNNEL=false # 默认即为 false,仅发起轻量 HTTP 登记,零长连接开销
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## 🔒 安全性考量
|
|
218
|
+
|
|
219
|
+
1. **防恶意伪造(SSRF 防御)**:在直连上报模式下,管理端强制从底层 TCP 握手来源直接锁定节点的真实来源 IP,杜绝假冒内网私网地址。
|
|
220
|
+
2. **防公网扫描冒充**:未携带 `JOIN_TOKEN` 的未知连接一律进入隔离待审核队列(TOFU),未经管理端管理员后台显式认领的节点,绝不参与任何数据交互。
|
|
221
|
+
3. **安全通信**:长连接反向通道强制基于 TLS (WSS) 传输,天然防御中间人窃听与数据篡改。
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## 📄 License
|
|
226
|
+
|
|
227
|
+
MIT License © 2026 [Wimotek](https://github.com/wimotek)
|
|
228
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
agent.py
|
|
3
|
+
pyproject.toml
|
|
4
|
+
wimotek_edge_agent.egg-info/PKG-INFO
|
|
5
|
+
wimotek_edge_agent.egg-info/SOURCES.txt
|
|
6
|
+
wimotek_edge_agent.egg-info/dependency_links.txt
|
|
7
|
+
wimotek_edge_agent.egg-info/entry_points.txt
|
|
8
|
+
wimotek_edge_agent.egg-info/requires.txt
|
|
9
|
+
wimotek_edge_agent.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agent
|