feat(agent): 增加 HTTPS pin 模式并修复隧道端口竞态
CI / verify (push) Canceled after 0s

This commit is contained in:
2026-09-28 14:13:57 +08:00
parent 1bec91cc5e
commit fac9234e4d
23 changed files with 648 additions and 77 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "fonu-web",
"version": "1.4.0",
"version": "1.4.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "fonu-web",
"version": "1.4.0",
"version": "1.4.1",
"dependencies": {
"@codemirror/commands": "^6.11.1",
"@codemirror/lang-javascript": "^6.2.5",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "fonu-web",
"private": true,
"version": "1.4.0",
"version": "1.4.1",
"packageManager": "npm@10.9.4",
"type": "module",
"scripts": {
+5
View File
@@ -307,6 +307,11 @@ export const api = {
testFrpAgent: (id: number) => request<{ ok: boolean }>(`/api/frpmulti/servers/${id}/agent/test`, { method: 'POST' }),
restartFrpAgentTunnel: (id: number) =>
request<FrpServer>(`/api/frpmulti/servers/${id}/agent/tunnel/restart`, { method: 'POST' }),
setFrpAgentTransport: (id: number, transport: 'tunnel' | 'https-pin', port = 7443) =>
request<FrpServer>(`/api/frpmulti/servers/${id}/agent/transport`, {
method: 'POST',
body: JSON.stringify({ transport, port }),
}),
probeFrpAgent: (id: number) => request<Record<string, any>>(`/api/frpmulti/servers/${id}/agent/probe`, { method: 'POST' }),
installFrpAgent: (id: number) => request<FrpServer>(`/api/frpmulti/servers/${id}/agent/install`, { method: 'POST' }),
uninstallFrpAgent: (id: number, keepData: boolean) => request<{ ok: boolean; output: string }>(`/api/frpmulti/servers/${id}/agent/uninstall?keep_data=${keepData}`, { method: 'POST' }),
+6
View File
@@ -854,6 +854,12 @@ export interface FrpAgentRouteDetail {
export interface FrpAgentStatus {
agent_version?: string
listen_addr?: string
listen_scope?: string
remote_access_enabled?: boolean
transport_security?: string
https_proxy_configured?: boolean
https_proxy_port?: number
ports?: Record<string, boolean>
frps_active?: boolean
frps_enabled?: boolean
+29 -1
View File
@@ -253,10 +253,25 @@
<n-alert v-if="server.agent_transport === 'http'" type="warning" :bordered="false" class="agent-transport-warning">
当前仍是公网明文 HTTP。点击「升级 Agent」可自动迁移到 SSH 隧道安全模式。
</n-alert>
<n-alert v-if="server.agent_transport === 'https-pin'" type="info" :bordered="false" class="agent-transport-warning">
HTTPS 直连使用证书指纹校验。请确认云安全组已放行配置的 TCP 端口,Fonu 不会自动修改云安全组。
</n-alert>
<p v-if="server.agent_transport_error" class="form-hint agent-transport-error">{{ server.agent_transport_error }}</p>
<div class="agent-actions">
<n-button size="small" :loading="busy === 'agent-install'" :disabled="busy !== null && busy !== 'agent-install'" @click="installAgent(server)">{{ server.agent_configured ? '升级 Agent' : '安装 Agent' }}</n-button>
<n-button v-if="server.agent_transport === 'tunnel'" size="small" :loading="busy === 'tunnel-restart'" :disabled="busy !== null && busy !== 'tunnel-restart'" @click="restartAgentTunnel(server)">重启隧道</n-button>
<n-popconfirm v-if="server.agent_transport === 'tunnel'" @positive-click="enableAgentHTTPS(server)">
<template #trigger>
<n-button size="small" :loading="busy === 'transport-https'" :disabled="busy !== null && busy !== 'transport-https'">切换 HTTPS 直连</n-button>
</template>
将使用自签名证书启用 TCP 7443。请确保云安全组已放行该端口,否则切换后 Agent 会不可达。继续?
</n-popconfirm>
<n-popconfirm v-if="server.agent_transport === 'https-pin'" @positive-click="switchAgentToTunnel(server)">
<template #trigger>
<n-button size="small" :loading="busy === 'transport-tunnel'" :disabled="busy !== null && busy !== 'transport-tunnel'">切回 SSH 隧道</n-button>
</template>
将关闭 VPS 上的 HTTPS Agent 入口并恢复 SSH 隧道。继续?
</n-popconfirm>
<n-button size="small" :loading="busy === 'diagnose'" :disabled="busy !== null && busy !== 'diagnose'" @click="diagnoseAgent(server)">SSH 诊断</n-button>
<n-button size="small" quaternary :loading="busy === 'agent-uninstall'" :disabled="busy !== null && busy !== 'agent-uninstall'" @click="uninstallAgent(server, true)">卸载(保留路由/证书)</n-button>
<n-popconfirm @positive-click="uninstallAgent(server, false)">
@@ -685,7 +700,8 @@ use([CanvasRenderer, LineChart, ScatterChart, GridComponent, TooltipComponent])
// 操作动作:按动作粒度控制 loading,保证"只有被点的按钮转圈"
type Action =
| 'probe' | 'agent-install' | 'agent-uninstall' | 'diagnose'
| 'tunnel-restart' | 'frps-start' | 'frps-stop' | 'frps-restart' | 'frps-install' | 'push-config'
| 'tunnel-restart' | 'transport-https' | 'transport-tunnel'
| 'frps-start' | 'frps-stop' | 'frps-restart' | 'frps-install' | 'push-config'
interface OpOutput { at: string; source: string; text: string; ok: boolean }
@@ -1413,6 +1429,18 @@ function restartAgentTunnel(target: FrpServer) {
.then(async (result) => { if (result !== undefined) await load() })
}
function enableAgentHTTPS(target: FrpServer) {
return run('transport-https', '切换 HTTPS 直连', () => api.setFrpAgentTransport(target.id, 'https-pin', 7443),
() => ({ text: 'HTTPS 直连已启用并通过证书指纹验证', ok: true }))
.then(async (result) => { if (result !== undefined) await load() })
}
function switchAgentToTunnel(target: FrpServer) {
return run('transport-tunnel', '切回 SSH 隧道', () => api.setFrpAgentTransport(target.id, 'tunnel'),
() => ({ text: '已关闭 HTTPS 直连并恢复 SSH 隧道', ok: true }))
.then(async (result) => { if (result !== undefined) await load() })
}
function agentTransportText(server: FrpServer) {
if (server.agent_transport === 'tunnel') return 'SSH 隧道'
if (server.agent_transport === 'https-pin') return 'HTTPS + 证书固定'
+8 -8
View File
@@ -8,7 +8,7 @@
</template>
</PageHeader>
<n-alert type="success" :bordered="false" class="example-alert" title="v1.4.0 示例页面验证标识已生效">
<n-alert type="success" :bordered="false" class="example-alert" title="v1.4.1 示例页面验证标识已生效">
当前页面由 v{{ version }} 提供,验证标识为 <span class="mono">{{ verificationMarker }}</span>。
如果你是在升级后首次看到这行内容,说明前端构建、镜像重建和服务重启均已成功。
</n-alert>
@@ -19,7 +19,7 @@
<strong class="example-proof__value">{{ verificationRound }}</strong>
</div>
<div class="example-proof__hint">
看到这个新区块,说明前端资源已经从旧版本切换到 v1.4.0。
看到这个新区块,说明前端资源已经从旧版本切换到 v1.4.1。
</div>
</div>
@@ -33,16 +33,16 @@
<StatCard label="本次验证标识" :value="verificationMarker" sub="用于区分升级前后的前端资源" tone="blue" value-small>
<template #icon><n-icon :component="FlashOutline" /></template>
</StatCard>
<StatCard label="页面状态" value="已更新" sub="v1.4.0 前端资源已加载" tone="teal">
<StatCard label="页面状态" value="已更新" sub="v1.4.1 前端资源已加载" tone="teal">
<template #icon><n-icon :component="CheckmarkCircleOutline" /></template>
</StatCard>
</div>
<FonuCard title="本次示例页变化" subtitle="用于确认升级后的页面内容确实发生变化">
<ul class="example-changes">
<li>页面版本提示更新为 v1.4.0。</li>
<li>「本次升级验证」区块和页面状态同步更新为 v1.4.0。</li>
<li>验证标识更新为 <span class="mono">v1.4.0-example-page</span>。</li>
<li>页面版本提示更新为 v1.4.1。</li>
<li>「本次升级验证」区块和页面状态同步更新为 v1.4.1。</li>
<li>验证标识更新为 <span class="mono">v1.4.1-example-page</span>。</li>
</ul>
</FonuCard>
@@ -87,8 +87,8 @@ const message = useMessage()
const version = ref(__APP_VERSION__)
const checkedAt = ref('')
const refreshing = ref(false)
const verificationRound = 'v1.4.0'
const verificationMarker = 'v1.4.0-example-page'
const verificationRound = 'v1.4.1'
const verificationMarker = 'v1.4.1-example-page'
async function refreshVersion() {
refreshing.value = true