feat(frp): frps vhost 回源端口支持 80/443,公网反代在该配置下不再适用
- 服务端表单不再硬性拒绝 vhostHTTPPort=80 / vhostHTTPSPort=443,改为保存时弹窗二次确认; 确认后请求携带 allow_privileged_vhost_ports 提交并由后端校验放行,未携带标记的请求(含直接调 API)仍被拒绝 - frps.toml 生成改为写入风险注释、不再报错;各规则的公网反代回源地址随 80/443 自动跟随 - 「公网反代」页检测到 80/443 后显示说明卡片并隐藏统计/Nginx/规则台账/漂移区块, 同时跳过 agent 的路由、漂移与健康请求 - 后端 DeployAgentRoute 与 RedeployAllRoutes(含漂移一键修复)拒绝下发,自动下发路径 syncAgentRoute 跳过 - 新增 internal/frp/vhost_port_test.go:校验、二次确认、frps.toml 生成、反代回源跟随与守卫对照
This commit is contained in:
@@ -676,6 +676,8 @@ export interface FrpServerPayload {
|
||||
clear_tls_certificate?: boolean
|
||||
clear_tls_key?: boolean
|
||||
clear_tls_trusted_ca?: boolean
|
||||
/** 二次确认标记:vhost 回源端口使用 80/443 时必须为 true(前端弹窗确认后置位)*/
|
||||
allow_privileged_vhost_ports?: boolean
|
||||
}
|
||||
|
||||
export interface FrpAgentRouteCandidate {
|
||||
|
||||
@@ -492,7 +492,7 @@
|
||||
:show-icon="false"
|
||||
style="margin-bottom: 12px"
|
||||
>
|
||||
80/443 通常由 VPS 上的 Nginx 占用,frps 不能再次监听;请改为 8080/8443,否则 frps 会因端口冲突启动失败。
|
||||
80/443 通常由 VPS 上的 Nginx 占用。继续使用前请先停止占用该端口的服务,保存时会再次弹窗确认;端口仍被占用时 frps 会因冲突启动失败。若 VPS 上还要保留 Nginx,请改用 8080/8443。
|
||||
</n-alert>
|
||||
<div class="form-grid">
|
||||
<n-form-item label="HTTP 回源端口"><n-input-number v-model:value="serverForm.options.vhost_http_port" :min="1" :max="65535" placeholder="默认 8080" /></n-form-item>
|
||||
@@ -1264,6 +1264,8 @@ const serverStartingId = ref<number | null>(null)
|
||||
const agentActionId = ref<number | null>(null)
|
||||
const measuringId = ref<number | null>(null)
|
||||
const savingServer = ref(false)
|
||||
// 80/443 回源端口的二次确认结果,按弹窗会话记录,避免每次保存都重复弹窗
|
||||
const confirmedPrivilegedVhost = ref('')
|
||||
|
||||
const showServerModal = ref(false)
|
||||
|
||||
@@ -1356,7 +1358,7 @@ const serverHelpItems = computed(() => {
|
||||
{ title: '连接调优', text: '心跳与 TCP 多路复用保持默认即可;网络不稳定时可加大超时,连接池可降低高并发时的建连延迟。' },
|
||||
]
|
||||
if (serverFormTab.value === 'server') return [
|
||||
{ title: 'frps vhost 与域名反代', text: 'vhostHTTPPort/vhostHTTPSPort 是 Nginx 反代的回源端口,同时决定 subDomainHost、tcpmux 等域名入口;留空用默认 8080/8443,80/443 会与 VPS 上的 Nginx 冲突。修改后需「下发 frps 配置」并重启 frps,再重新部署各规则。' },
|
||||
{ title: 'frps vhost 与域名反代', text: 'vhostHTTPPort/vhostHTTPSPort 是 Nginx 反代的回源端口,同时决定 subDomainHost、tcpmux 等域名入口;留空用默认 8080/8443。80/443 通常与 VPS 上的 Nginx 冲突,确需使用时保存前会二次确认,请先释放该端口。修改后需「下发 frps 配置」并重启 frps,再重新部署各规则。' },
|
||||
{ title: 'TCP/UDP 端口分配', text: 'allowPorts 只限制 tcp/udp 规则使用的 remotePort,不控制 vhost 回源端口;实际端口仍需在云安全组与 VPS 主机防火墙放行。' },
|
||||
{ title: 'frps 管理接口', text: 'frps 开启 webServer(dashboard)后填写,Havline 借此拉取穿透规则的实时连接与流量;地址与端口需成对填写,与连接认证无关。' },
|
||||
]
|
||||
@@ -1463,6 +1465,7 @@ function resetServerForm() {
|
||||
function openServerModal(server?: FrpServer) {
|
||||
editingServer.value = server ?? null
|
||||
serverFormTab.value = 'basic'
|
||||
confirmedPrivilegedVhost.value = ''
|
||||
resetServerForm()
|
||||
if (server) {
|
||||
Object.assign(serverForm, { ...server, token: '', oidc_client_secret: '', agent_token: '', ssh_secret: '' })
|
||||
@@ -1671,10 +1674,7 @@ function validateServerForm(): string {
|
||||
if (!editingServer.value && !form.oidc_client_secret.trim()) return 'OIDC 需填写客户端密钥'
|
||||
}
|
||||
|
||||
if (form.options.vhost_http_port === 80 || form.options.vhost_https_port === 443) {
|
||||
return 'frps 回源端口不能使用 80/443(通常由 VPS 上的 Nginx 占用),请改为 8080/8443'
|
||||
}
|
||||
|
||||
// 80/443 不再硬拦:由 saveServer 弹窗二次确认,后端要求同一次请求带上确认标记
|
||||
const ports: Array<[string, number]> = [
|
||||
['控制端口', form.server_port],
|
||||
['HTTP 回源端口', form.options.vhost_http_port || 8080],
|
||||
@@ -1708,12 +1708,41 @@ function validateServerForm(): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
// 当前表单里用到的 80/443 回源端口;空数组表示无需确认。
|
||||
function privilegedVhostPorts(): number[] {
|
||||
const ports: number[] = []
|
||||
if (serverForm.options.vhost_http_port === 80) ports.push(80)
|
||||
if (serverForm.options.vhost_https_port === 443) ports.push(443)
|
||||
return ports
|
||||
}
|
||||
|
||||
// 80/443 通常已被 VPS 上的 Nginx 占用,放行前必须让用户确认自己已经释放了端口。
|
||||
function confirmPrivilegedVhostPorts(ports: number[]): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
dialog.warning({
|
||||
title: `确认使用 ${ports.join(' / ')} 作为 frps 回源端口`,
|
||||
content: `${ports.join(' / ')} 通常已被 VPS 上的 Nginx 占用。继续前请先停止占用该端口的服务,否则 frps 会因端口冲突启动失败,隧道和公网反代都会不可用。保存后需在服务端详情「下发 frps 配置」并重启 frps 才生效。`,
|
||||
positiveText: '端口已释放,继续保存',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => resolve(true),
|
||||
onNegativeClick: () => resolve(false),
|
||||
onClose: () => resolve(false),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function saveServer() {
|
||||
const invalid = validateServerForm()
|
||||
if (invalid) {
|
||||
message.error(invalid)
|
||||
return
|
||||
}
|
||||
const privileged = privilegedVhostPorts()
|
||||
if (privileged.length > 0 && confirmedPrivilegedVhost.value !== privileged.join(',')) {
|
||||
const confirmed = await confirmPrivilegedVhostPorts(privileged)
|
||||
if (!confirmed) return
|
||||
confirmedPrivilegedVhost.value = privileged.join(',')
|
||||
}
|
||||
if (serverForm.mgmt_enabled && !serverForm.agent_url.trim()) {
|
||||
message.warning('未填写 Agent 地址:保存后可在本页通过 SSH 安装 Agent 自动回填')
|
||||
}
|
||||
@@ -1730,6 +1759,7 @@ async function saveServer() {
|
||||
tls_certificate: serverForm.tls_certificate, tls_key: serverForm.tls_key, tls_trusted_ca: serverForm.tls_trusted_ca,
|
||||
clear_tls_certificate: serverForm.clear_tls_certificate, clear_tls_key: serverForm.clear_tls_key,
|
||||
clear_tls_trusted_ca: serverForm.clear_tls_trusted_ca,
|
||||
allow_privileged_vhost_ports: privileged.length > 0,
|
||||
}
|
||||
if (editingServer.value) await api.updateFrpServer(editingServer.value.id, payload)
|
||||
else await api.createFrpServer(payload)
|
||||
|
||||
@@ -61,8 +61,18 @@
|
||||
<!-- 数据读取失败:必须可见(原先 loadErrorMsg 写了但从未渲染) -->
|
||||
<n-alert v-if="loadErrorMsg" type="error" :bordered="false" class="page-alert">{{ loadErrorMsg }}</n-alert>
|
||||
|
||||
<!-- frps 回源端口 80/443:VPS 上由 frps 直接对外,公网反代没有位置(也不需要自动部署) -->
|
||||
<n-alert v-if="publicProxyInapplicable" type="info" :bordered="false" class="page-alert">
|
||||
<div class="alert-body__text">
|
||||
<strong>该服务端的 frps 回源端口是 {{ privilegedVhostPorts.join(' / ') }},公网反代不适用</strong>
|
||||
<span>80/443 已由 frps 直接对外使用,VPS 上没有 Nginx 的位置:域名访问走 FRP 隧道直达 NAS,不需要再部署 Nginx 反代,本页也不会自动下发任何路由。</span>
|
||||
<span>如需使用公网反代,请到「公网服务端」页把回源端口改回 8080/8443,再回本页部署。</span>
|
||||
<span>若这台 VPS 上还留着以前部署的 Nginx 路由,需先把回源端口改回 8080/8443,才能在本页移除。</span>
|
||||
</div>
|
||||
</n-alert>
|
||||
|
||||
<!-- 部署漂移:库里的期望与 VPS 上的实际不一致时才出现(P0-2) -->
|
||||
<n-alert v-if="driftItems.length > 0" type="warning" :bordered="false" class="page-alert">
|
||||
<n-alert v-if="driftItems.length > 0 && !publicProxyInapplicable" type="warning" :bordered="false" class="page-alert">
|
||||
<div class="alert-body">
|
||||
<div class="alert-body__text">
|
||||
<strong>检测到 {{ driftItems.length }} 项部署漂移({{ driftSummary }})</strong>
|
||||
@@ -81,7 +91,7 @@
|
||||
</n-alert>
|
||||
|
||||
<!-- Nginx 前置条件:缺失 / 校验未通过 -->
|
||||
<n-alert v-if="nginxBlocking" :type="nginxBlocking.type" :bordered="false" class="page-alert">
|
||||
<n-alert v-if="nginxBlocking && !publicProxyInapplicable" :type="nginxBlocking.type" :bordered="false" class="page-alert">
|
||||
<div class="alert-body">
|
||||
<div class="alert-body__text">
|
||||
<strong>{{ nginxBlocking.title }}</strong>
|
||||
@@ -100,7 +110,7 @@
|
||||
</n-alert>
|
||||
|
||||
<!-- 入口行:agent / frps 生命周期在另一页 -->
|
||||
<div class="entrance-row">
|
||||
<div v-if="!publicProxyInapplicable" class="entrance-row">
|
||||
<span class="entrance-row__text">
|
||||
Agent 与 frps 的安装、升级、启停、配置下发在「公网服务端」页;本页只负责把规则发布到 Nginx
|
||||
</span>
|
||||
@@ -108,7 +118,7 @@
|
||||
</div>
|
||||
|
||||
<!-- ① 统计区 -->
|
||||
<section class="stats-row" aria-label="反代概览">
|
||||
<section v-if="!publicProxyInapplicable" class="stats-row" aria-label="反代概览">
|
||||
<div class="stat-card">
|
||||
<n-icon :component="LayersOutline" class="stat-card__icon stat-card__icon--blue" aria-hidden="true" />
|
||||
<div class="stat-card__value">{{ candidates.length }}</div>
|
||||
@@ -136,7 +146,7 @@
|
||||
</section>
|
||||
|
||||
<!-- ② 环境与 Nginx -->
|
||||
<HavlineCard class="section-card nginx-card" title="Nginx(公网反代依赖)">
|
||||
<HavlineCard v-if="!publicProxyInapplicable" class="section-card nginx-card" title="Nginx(公网反代依赖)">
|
||||
<div class="section-body">
|
||||
<div class="field-grid">
|
||||
<div><label>可用性</label><span class="field-tags"><StatusBadge :kind="nginxKind" :text="nginxText" /></span></div>
|
||||
@@ -170,7 +180,7 @@
|
||||
</HavlineCard>
|
||||
|
||||
<!-- ③ 规则台账 -->
|
||||
<HavlineCard class="section-card" title="规则台账">
|
||||
<HavlineCard v-if="!publicProxyInapplicable" class="section-card" title="规则台账">
|
||||
<div class="section-body">
|
||||
<p class="form-hint">穿透规则保存后不会自动改 VPS:点「部署反代」才会发布到 Nginx(自动推送证书并做回源自检)</p>
|
||||
<div v-if="candidates.length" class="route-table-wrap">
|
||||
@@ -485,6 +495,18 @@ const rollingBack = ref(false)
|
||||
const server = computed(() => servers.value.find((item) => item.id === selectedId.value) ?? null)
|
||||
const agentStatusOk = computed(() => statusLoaded.value && agentStatus.value !== null)
|
||||
|
||||
// frps 回源端口落在 80/443 时,VPS 上由 frps 直接对外,没有 Nginx 的位置:
|
||||
// 公网反代整体不适用,也不需要(更不应该)自动部署。
|
||||
const privilegedVhostPorts = computed<string[]>(() => {
|
||||
const options = server.value?.options
|
||||
if (!options) return []
|
||||
const ports: string[] = []
|
||||
if ((options.vhost_http_port || 8080) === 80) ports.push('80')
|
||||
if ((options.vhost_https_port || 8443) === 443) ports.push('443')
|
||||
return ports
|
||||
})
|
||||
const publicProxyInapplicable = computed(() => privilegedVhostPorts.value.length > 0)
|
||||
|
||||
// 统计:已生效 / 待处理 / 健康通过 / 当前连接
|
||||
const deployedCount = computed(() => candidates.value.filter((item) => stateOf(item).state === 'loaded').length)
|
||||
const pendingCount = computed(() => candidates.value.length - deployedCount.value)
|
||||
@@ -541,6 +563,16 @@ async function load() {
|
||||
async function refreshAll(options: { silent?: boolean } = { silent: true }) {
|
||||
const current = server.value
|
||||
if (!current) return
|
||||
// 80/443 回源端口下公网反代不适用:清空台账,也不必再去问 agent 的路由/漂移/健康
|
||||
if (publicProxyInapplicable.value) {
|
||||
candidates.value = []
|
||||
routeDetails.value = []
|
||||
routeHealth.value = []
|
||||
routeDrift.value = null
|
||||
agentStatus.value = null
|
||||
statusLoaded.value = false
|
||||
return
|
||||
}
|
||||
if (options.silent === false) manualRefreshing.value = true
|
||||
const failures: string[] = []
|
||||
const track = async <T>(label: string, fn: () => Promise<T>, assign: (value: T) => void) => {
|
||||
|
||||
Reference in New Issue
Block a user