feat: add OpenConsoleProxy service facade

This commit is contained in:
2026-07-29 12:55:36 +01:00
parent ba2e263d00
commit 20a302f84a
+122
View File
@@ -0,0 +1,122 @@
package services
import (
"errors"
"fmt"
"log"
"net"
"os"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/grpc/pb"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/proxy"
)
// ErrAgentOffline means the console cannot be opened because the target's agent
// is not on the command stream. Every console session is relayed by the agent,
// so this is fatal rather than a degraded mode.
var ErrAgentOffline = errors.New("agent is not connected")
// ConsoleProxy is a pending relay: a bound listener guacd can dial and a
// dispatched command telling the agent to meet it.
type ConsoleProxy struct {
ProxyID string
Host string
Port int
session *proxy.Session
}
func (c *ConsoleProxy) Close() {
proxy.Default.Remove(c.ProxyID)
c.session.Close("")
}
func (c *ConsoleProxy) Reason() string { return c.session.Reason() }
func proxyListenHost() string {
if v := os.Getenv("PROXY_LISTEN_HOST"); v != "" {
return v
}
return "0.0.0.0"
}
func proxyAdvertiseHost() string {
if v := os.Getenv("PROXY_ADVERTISE_HOST"); v != "" {
return v
}
return "server"
}
func guacdAddr() string {
if v := os.Getenv("GUACD_ADDR"); v != "" {
return v
}
return "guacd:4822"
}
// guacdHosts resolves guacd's address to the IPs allowed to claim a relay
// listener. An unresolvable host yields an empty set, which allows any source:
// refusing everything would take the console down entirely, so the narrower
// protections (ephemeral port, 10s window, single accept) carry it instead.
func guacdHosts(addr string) []string {
host, _, err := net.SplitHostPort(addr)
if err != nil {
host = addr
}
if ip := net.ParseIP(host); ip != nil {
return []string{ip.String()}
}
ips, err := net.LookupHost(host)
if err != nil {
log.Printf("proxy: cannot resolve guacd host %q, allowing any relay source: %v", host, err)
return nil
}
return ips
}
// DispatchOpenProxy tells the agent to dial its own loopback on port and relay
// it back under proxyID.
func DispatchOpenProxy(serverID, proxyID string, port uint32) error {
return Dispatcher.dispatch(serverID, &pb.ServerCommand{
CommandId: proxyID,
OpenProxy: &pb.OpenProxyCmd{ProxyId: proxyID, Port: port},
})
}
// OpenConsoleProxy binds a relay listener, registers it, and asks the agent to
// connect. The caller must Close the result.
func OpenConsoleProxy(instanceID, serverID string, targetPort int) (*ConsoleProxy, error) {
if !Dispatcher.IsConnected(serverID) {
return nil, ErrAgentOffline
}
proxyID, err := proxy.NewID()
if err != nil {
return nil, fmt.Errorf("generate proxy id: %w", err)
}
sess, err := proxy.NewSession(proxyListenHost(), guacdHosts(guacdAddr()))
if err != nil {
return nil, err
}
proxy.Default.Add(&proxy.Entry{
ProxyID: proxyID,
InstanceID: instanceID,
ServerID: serverID,
Session: sess,
})
if err := DispatchOpenProxy(serverID, proxyID, uint32(targetPort)); err != nil {
proxy.Default.Remove(proxyID)
sess.Close("dispatch_failed")
return nil, err
}
return &ConsoleProxy{
ProxyID: proxyID,
Host: proxyAdvertiseHost(),
Port: sess.Port(),
session: sess,
}, nil
}