refactor(proxy): 将 aether-proxy 从 HMAC 正向代理迁移到 WebSocket 隧道模式

移除 HMAC 认证、TLS 自签名证书、HTTP CONNECT 代理和代发(delegate)模式,
改为 aether-proxy 主动通过 WebSocket 连接 Aether 服务端建立隧道。

Aether 服务端新增:
- WebSocket 隧道端点 (proxy_tunnel.py)
- TunnelManager 管理隧道连接和请求分发
- TunnelTransport 作为 httpx 自定义 transport 层
- 基于二进制帧的隧道协议 (tunnel_protocol.py)

aether-proxy (Rust) 重构:
- 新增 tunnel 模块 (client/dispatcher/stream_handler/protocol)
- 支持多 Aether 服务端连接 ([[servers]] 配置)
- 移除 proxy/auth/delegate 模块和 hyper 依赖
- 改用 tokio-tungstenite 实现 WebSocket 客户端

同时:
- 添加浏览器指纹 Headers 绕过 Cloudflare 防护
- 删除节点时自动清理 Provider/Endpoint 的代理引用
- 数据库迁移: 新增 tunnel_mode/tunnel_connected/tunnel_connected_at 字段
This commit is contained in:
fawney19
2026-02-25 21:59:29 +08:00
parent 39b036abd5
commit fd9040b9aa
53 changed files with 2938 additions and 2728 deletions

View File

@@ -5,7 +5,6 @@ export interface ProxyNodeRemoteConfig {
allowed_ports?: number[]
log_level?: string
heartbeat_interval?: number
timestamp_tolerance?: number
}
export interface ProxyNode {
@@ -16,6 +15,9 @@ export interface ProxyNode {
region: string | null
status: 'online' | 'unhealthy' | 'offline'
is_manual: boolean
tunnel_mode: boolean
tunnel_connected: boolean
tunnel_connected_at: string | null
// 手动节点专用字段
proxy_url?: string
proxy_username?: string
@@ -101,9 +103,4 @@ export const proxyNodesApi = {
const response = await apiClient.post<ProxyNodeTestResult>('/api/admin/proxy-nodes/test-url', data)
return response.data
},
async getHmacKey(): Promise<{ proxy_hmac_key: string }> {
const response = await apiClient.get<{ proxy_hmac_key: string }>('/api/admin/proxy-nodes/hmac-key')
return response.data
},
}

View File

@@ -95,16 +95,6 @@
</SelectContent>
</Select>
<div class="h-4 w-px bg-border" />
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
title="复制 HMAC Key"
@click="copyHmacKey"
>
<Copy class="w-3.5 h-3.5" />
</Button>
<div class="h-4 w-px bg-border" />
<Button
variant="ghost"
size="icon"
@@ -172,11 +162,18 @@
>
手动
</Badge>
<Badge
v-if="node.tunnel_mode"
variant="outline"
class="text-[10px] px-1.5 py-0"
>
Tunnel
</Badge>
<HardwareTooltip :node="node" />
</div>
</TableCell>
<TableCell class="py-4">
<code class="text-xs text-muted-foreground">{{ node.is_manual ? (node.proxy_url || `${node.ip}:${node.port}`) : `${node.ip}:${node.port}` }}</code>
<code class="text-xs text-muted-foreground">{{ nodeAddress(node) }}</code>
</TableCell>
<TableCell class="py-4">
<span class="text-sm text-muted-foreground">{{ formatRegion(node.region) }}</span>
@@ -282,9 +279,16 @@
>
手动
</Badge>
<Badge
v-if="node.tunnel_mode"
variant="outline"
class="text-[10px] px-1.5 py-0"
>
Tunnel
</Badge>
<HardwareTooltip :node="node" />
</div>
<code class="text-xs text-muted-foreground">{{ node.is_manual ? (node.proxy_url || `${node.ip}:${node.port}`) : `${node.ip}:${node.port}` }}</code>
<code class="text-xs text-muted-foreground">{{ nodeAddress(node) }}</code>
</div>
<Badge
:variant="statusVariant(node.status)"
@@ -521,15 +525,6 @@
max="600"
/>
</div>
<div class="space-y-1.5">
<Label>时间戳容差 ()</Label>
<Input
v-model="configForm.timestamp_tolerance"
type="number"
min="10"
max="3600"
/>
</div>
</div>
<div
v-if="configNode"
@@ -560,7 +555,6 @@
import { ref, computed, onMounted, watch } from 'vue'
import { useProxyNodesStore } from '@/stores/proxy-nodes'
import { useToast } from '@/composables/useToast'
import { useClipboard } from '@/composables/useClipboard'
import { useConfirm } from '@/composables/useConfirm'
import { proxyNodesApi, type ProxyNode, type ProxyNodeRemoteConfig } from '@/api/proxy-nodes'
@@ -586,13 +580,12 @@ import {
Dialog,
} from '@/components/ui'
import { Search, Trash2, Plus, SquarePen, Activity, Loader2, Settings, Copy } from 'lucide-vue-next'
import { Search, Trash2, Plus, SquarePen, Activity, Loader2, Settings } from 'lucide-vue-next'
import { parseApiError } from '@/utils/errorParser'
import { formatRegion } from '@/utils/region'
import HardwareTooltip from './components/HardwareTooltip.vue'
const { success, error: toastError } = useToast()
const { copyToClipboard } = useClipboard()
const { confirmDanger } = useConfirm()
const store = useProxyNodesStore()
@@ -621,7 +614,6 @@ const configForm = ref({
allowed_ports: '',
log_level: 'info',
heartbeat_interval: '30',
timestamp_tolerance: '300',
})
// 测试连通性
@@ -686,15 +678,6 @@ async function handleTestUrl() {
}
}
async function copyHmacKey() {
try {
const { proxy_hmac_key } = await proxyNodesApi.getHmacKey()
await copyToClipboard(proxy_hmac_key)
} catch (err: unknown) {
toastError(parseApiError(err, '获取 HMAC Key 失败'))
}
}
function handleEdit(node: ProxyNode) {
editingNode.value = node
addForm.value = {
@@ -766,7 +749,6 @@ function handleConfig(node: ProxyNode) {
allowed_ports: rc.allowed_ports?.join(', ') || '',
log_level: rc.log_level || 'info',
heartbeat_interval: String(rc.heartbeat_interval || node.heartbeat_interval || 30),
timestamp_tolerance: String(rc.timestamp_tolerance || 300),
}
showConfigDialog.value = true
}
@@ -800,10 +782,6 @@ async function handleSaveConfig() {
if (!isNaN(hb) && hb >= 5) {
data.heartbeat_interval = hb
}
const tt = parseInt(configForm.value.timestamp_tolerance)
if (!isNaN(tt) && tt >= 10) {
data.timestamp_tolerance = tt
}
await proxyNodesApi.updateNodeConfig(configNode.value.id, data)
success('远程配置已保存,将在下次心跳时生效')
handleConfigDialogClose(false)
@@ -817,7 +795,7 @@ async function handleSaveConfig() {
async function handleDelete(node: ProxyNode) {
const confirmed = await confirmDanger(
`确定要删除代理节点 "${node.name}" (${node.ip}:${node.port}) 吗?`,
`确定要删除代理节点 "${node.name}" (${node.tunnel_mode ? node.ip : `${node.ip}:${node.port}`}) 吗?`,
'删除节点'
)
if (!confirmed) return
@@ -890,4 +868,10 @@ function formatTime(iso: string | null) {
if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`
return d.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
}
function nodeAddress(node: ProxyNode) {
if (node.is_manual) return node.proxy_url || `${node.ip}:${node.port}`
if (node.tunnel_mode) return node.ip || 'WebSocket Tunnel'
return `${node.ip}:${node.port}`
}
</script>