From 1e5f35b9a3dbf688b9a42a01229d0d9d9b0b5b65 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 22 Jun 2026 14:22:20 +0800 Subject: [PATCH] fix(openflare): refresh WS read deadline on JSON pong; capture frpc stderr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - read_pump: 收到客户端 JSON {"type":"pong"} 时调用 conn.SetReadDeadline 刷新 服务端读超时。修复前,服务端仅在 WebSocket 协议层 Pong 帧时刷新 deadline, 而客户端使用 JSON 应用层 pong 回复,导致服务端 90s 后超时关闭连接, 客户端收到 EOF 并触发无限重连循环。在 Cloudflare 代理场景下, 100s 空闲超时进一步加剧了此问题。 - frpc/manager: 捕获 frpc 子进程 stderr 并在进程异常退出时 将其内容记录到结构化日志 stderr 字段,便于诊断 exit status 1 的 具体原因(如配置格式错误、Auth Token 失败、relay 服务端不可达等)。 --- internal/apps/flared/frpc/manager.go | 9 ++++++++- internal/apps/openflare/websocket/read_pump.go | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/internal/apps/flared/frpc/manager.go b/internal/apps/flared/frpc/manager.go index abc4a127..2b0c9598 100644 --- a/internal/apps/flared/frpc/manager.go +++ b/internal/apps/flared/frpc/manager.go @@ -192,6 +192,8 @@ func (m *Manager) restartProcess(ctx context.Context, relayID string, configPath ensureNoOrphanProcess(pidPath) cmd := exec.CommandContext(procCtx, m.cfg.FrpcPath, "-c", configPath) //nolint:gosec // FrpcPath and configPath are managed trusted locations + var stderrBuf bytes.Buffer + cmd.Stderr = &stderrBuf m.mu.Lock() proc.Cmd = cmd @@ -213,10 +215,15 @@ func (m *Manager) restartProcess(ctx context.Context, relayID string, configPath return } + stderrOutput := strings.TrimSpace(stderrBuf.String()) if err != nil { proc.LastError = err.Error() proc.Status = "error" - slog.Error("frpc process exited unexpectedly", "relay_id", relayID, "error", err) + if stderrOutput != "" { + slog.Error("frpc process exited unexpectedly", "relay_id", relayID, "error", err, "stderr", stderrOutput) + } else { + slog.Error("frpc process exited unexpectedly", "relay_id", relayID, "error", err) + } } else { proc.Status = "stopped" proc.LastError = "exited unexpectedly with code 0" diff --git a/internal/apps/openflare/websocket/read_pump.go b/internal/apps/openflare/websocket/read_pump.go index d8da94e8..829ae330 100644 --- a/internal/apps/openflare/websocket/read_pump.go +++ b/internal/apps/openflare/websocket/read_pump.go @@ -42,6 +42,13 @@ func runReadPump( case messageTypePing: _ = sendPong(nodeID) case clientPongType: + // Refresh read deadline when the client replies with a JSON pong. + // This keeps the connection alive when the WebSocket is proxied + // through Cloudflare, which enforces a 100-second idle timeout on + // the TCP stream. Without this refresh, the server's 90-second read + // deadline expires and terminates the connection even though the + // client is actively responding to pings. + _ = conn.SetReadDeadline(time.Now().Add(wsReadDeadline)) default: slog.Debug(logLabel+" unsupported message", "node_id", nodeID, "type", message.Type) }