feat: route every console session through the agent relay

This commit is contained in:
2026-07-29 13:07:30 +01:00
parent 59d147fe4d
commit c0bec3737b
2 changed files with 63 additions and 13 deletions
+35 -2
View File
@@ -1,6 +1,7 @@
package api
import (
"errors"
"net"
"net/http"
"os"
@@ -33,6 +34,15 @@ func consoleConnect(c *gin.Context) {
return
}
if srv.Status != "active" {
c.JSON(http.StatusConflict, gin.H{
"error": "agent_offline",
"message": "The agent on this server is not connected. " +
"Console sessions are relayed by the agent, so it must be online.",
})
return
}
sess, err := services.CreateConsoleSession(auth.InstanceID(c), body.ServerID, body.Protocol, body.KeyID, actorFromCtx(c), c.ClientIP())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
@@ -59,7 +69,7 @@ func consoleConnect(c *gin.Context) {
}
services.LogEvent(auth.InstanceID(c), "console.opened", actorFromCtx(c), srv.ServerID, "",
"console session opened ("+body.Protocol+")")
"console session opened ("+body.Protocol+", agent-relayed)")
c.JSON(http.StatusOK, gin.H{
"session_id": sess.SessionID,
@@ -124,7 +134,25 @@ func consoleTunnel(c *gin.Context) {
return
}
}
gp, err := services.BuildGuacParams(srv, sess.Protocol, sess.SSHUsername, privKey, passphrase, rdpUser, rdpPass)
targetPort, err := services.TargetPort(srv, sess.Protocol)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
relay, err := services.OpenConsoleProxy(instanceID, srv.ServerID, targetPort)
if err != nil {
if errors.Is(err, services.ErrAgentOffline) {
c.JSON(http.StatusConflict, gin.H{"error": "agent_offline"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not open relay"})
return
}
defer relay.Close()
gp, err := services.BuildGuacParams(sess.Protocol, sess.SSHUsername, privKey, passphrase,
rdpUser, rdpPass, relay.Host, relay.Port)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
@@ -162,6 +190,11 @@ func consoleTunnel(c *gin.Context) {
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, "",
"console relay failed: "+reason)
}
relay.Close()
_ = services.EndConsoleSession(instanceID, sessionID)
}
wsServer.ServeHTTP(c.Writer, c.Request)
+28 -11
View File
@@ -77,20 +77,37 @@ type GuacParams struct {
Params map[string]string
}
func portOr(v, def int) string {
func portOr(v, def int) int {
if v == 0 {
v = def
return def
}
return strconv.Itoa(v)
return v
}
func BuildGuacParams(srv *models.Server, protocol, sshUser, privateKey, passphrase, rdpUser, rdpPass string) (*GuacParams, error) {
host := srv.IPAddress
// TargetPort is the port on the managed server that the agent will dial on its
// own loopback address.
func TargetPort(srv *models.Server, protocol string) (int, error) {
switch protocol {
case "ssh":
return portOr(srv.SSHPort, 22), nil
case "rdp":
return portOr(srv.RDPPort, 3389), nil
case "vnc":
return 5900, nil
default:
return 0, fmt.Errorf("unsupported protocol %q", protocol)
}
}
// BuildGuacParams points guacd at the relay listener, never at the managed
// server: on a cloud deployment the server's address is not routable from here.
func BuildGuacParams(protocol, sshUser, privateKey, passphrase, rdpUser, rdpPass, relayHost string, relayPort int) (*GuacParams, error) {
port := strconv.Itoa(relayPort)
switch protocol {
case "ssh":
p := map[string]string{
"hostname": host,
"port": portOr(srv.SSHPort, 22),
"hostname": relayHost,
"port": port,
}
if sshUser == "" {
sshUser = "root"
@@ -105,8 +122,8 @@ func BuildGuacParams(srv *models.Server, protocol, sshUser, privateKey, passphra
return &GuacParams{Protocol: "ssh", Params: p}, nil
case "rdp":
return &GuacParams{Protocol: "rdp", Params: map[string]string{
"hostname": host,
"port": portOr(srv.RDPPort, 3389),
"hostname": relayHost,
"port": port,
"username": rdpUser,
"password": rdpPass,
"security": "any",
@@ -114,8 +131,8 @@ func BuildGuacParams(srv *models.Server, protocol, sshUser, privateKey, passphra
}}, nil
case "vnc":
return &GuacParams{Protocol: "vnc", Params: map[string]string{
"hostname": host,
"port": "5900",
"hostname": relayHost,
"port": port,
"password": rdpPass,
}}, nil
default: