fix: bound and complete console relay teardown, restore proxy_failed audit

- Arm the unclaimed-relay watchdog in NewSession rather than Serve, so an
  agent that never opens its ProxyStream is bounded to 10s and reports
  reason "agent_timeout", per the design spec's failure-mode table.
- Session.Close now also closes the accepted net.Conn (stored via setConn),
  so ConsoleProxy.Close() is an unconditional kill of the whole relay chain
  instead of only closing an already-idle listener.
- Emit console.proxy_failed and end the console session from a defer in
  consoleTunnel guarded on relay.Reason(), since guac's OnDisconnect never
  runs when the connect callback errors -- which is the path every relay
  failure this feature introduces takes. Update the two docsite
  troubleshooting rows to match what the audit event can now actually show.
This commit is contained in:
2026-07-31 09:21:07 +01:00
parent 1e1546cb60
commit 1fe608f531
4 changed files with 68 additions and 16 deletions
+19 -9
View File
@@ -150,7 +150,22 @@ func consoleTunnel(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not open relay"})
return
}
defer relay.Close()
// guac.WebsocketServer.ServeHTTP returns before installing its
// OnDisconnect handler when the connect callback errors, which is exactly
// the path every relay failure this proxy introduces takes (the agent
// never claims it, dial_refused, guacd never dials, rejected). Emitting
// console.proxy_failed and ending the session here, unconditionally on
// teardown, is what makes those failures reach the audit log at all;
// OnDisconnect below only sees the rarer case of a session that was fully
// established and then failed.
defer func() {
relay.Close()
if reason := relay.Reason(); reason != "" {
services.LogEvent(instanceID, "console.proxy_failed", actorFromCtx(c), srv.ServerID, "",
fmt.Sprintf("console relay failed: %s (proxy_id=%s, port=%d)", reason, relay.ProxyID, relay.Port))
}
_ = services.EndConsoleSession(instanceID, sessionID)
}()
services.LogEvent(instanceID, "console.proxy_opened", actorFromCtx(c), srv.ServerID, "",
fmt.Sprintf("console relay opened (proxy_id=%s, port=%d)", relay.ProxyID, relay.Port))
@@ -192,14 +207,9 @@ func consoleTunnel(c *gin.Context) {
return guac.NewSimpleTunnel(stream), nil
}
// Teardown (proxy_failed audit + EndConsoleSession) lives in the deferred
// func above, not here: this only fires once a tunnel was actually
// established, and letting both paths log would double the audit event.
wsServer := guac.NewWebsocketServer(connect)
wsServer.OnDisconnect = func(id string, r *http.Request, t guac.Tunnel) {
if reason := relay.Reason(); reason != "" {
services.LogEvent(instanceID, "console.proxy_failed", actorFromCtx(c), srv.ServerID, "",
fmt.Sprintf("console relay failed: %s (proxy_id=%s, port=%d)", reason, relay.ProxyID, relay.Port))
}
relay.Close()
_ = services.EndConsoleSession(instanceID, sessionID)
}
wsServer.ServeHTTP(c.Writer, c.Request)
}