diff --git a/server/internal/api/console.go b/server/internal/api/console.go index 9df7d0b..945c80c 100644 --- a/server/internal/api/console.go +++ b/server/internal/api/console.go @@ -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) diff --git a/server/internal/services/console.go b/server/internal/services/console.go index 91c1abb..e001634 100644 --- a/server/internal/services/console.go +++ b/server/internal/services/console.go @@ -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: