yao/tai/vnc/vnc.go
Max 6408c140fc Implement tunnel mode support in Tai service
- Introduce a new "tunnel" scheme for the Tai client, allowing connections through Yao's reverse proxy.
- Enhance the Tai registry to manage tunnel-connected nodes and their ports.
- Add WebSocket and reverse proxy routes for tunnel connections in the OpenAPI server.
- Implement tunnel-specific proxy and VNC handling to facilitate communication with containerized environments.
- Update gRPC environment variable handling to support tunnel connections.

These changes improve the Tai service's flexibility and connectivity options, enabling better integration with remote and containerized environments.
2026-03-06 17:12:36 +08:00

122 lines
3 KiB
Go

package vnc
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/yaoapp/yao/tai/sandbox"
)
const defaultVNCContainerPort = 6080
// VNC resolves VNC WebSocket URLs for containers.
// Remote routes through Tai VNC router; Local resolves host ports directly.
type VNC interface {
URL(ctx context.Context, containerID string) (string, error)
Ping(ctx context.Context, containerID string) error
}
// --- Remote implementation ---
type remoteVNC struct {
host string
port int
client *http.Client
}
// NewRemote creates a VNC that routes through Tai's VNC router.
func NewRemote(host string, port int, hc *http.Client) VNC {
if hc == nil {
hc = http.DefaultClient
}
return &remoteVNC{host: host, port: port, client: hc}
}
func (r *remoteVNC) URL(_ context.Context, containerID string) (string, error) {
return fmt.Sprintf("ws://%s:%d/vnc/%s/ws", r.host, r.port, containerID), nil
}
func (r *remoteVNC) Ping(ctx context.Context, containerID string) error {
url := fmt.Sprintf("http://%s:%d/vnc/%s/ws", r.host, r.port, containerID)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := r.client.Do(req)
if err != nil {
return err
}
resp.Body.Close()
return nil
}
// --- Tunnel implementation ---
type tunnelVNC struct {
taiID string
yaoBase string // e.g. "http://yao-host:5099"
}
// NewTunnel creates a VNC that routes through Yao's reverse proxy
// for tunnel-connected Tai instances.
func NewTunnel(taiID, yaoBase string) VNC {
return &tunnelVNC{taiID: taiID, yaoBase: strings.TrimRight(yaoBase, "/")}
}
func (t *tunnelVNC) URL(_ context.Context, containerID string) (string, error) {
base := strings.Replace(t.yaoBase, "http://", "ws://", 1)
base = strings.Replace(base, "https://", "wss://", 1)
return fmt.Sprintf("%s/tai/%s/vnc/%s/ws", base, t.taiID, containerID), nil
}
func (t *tunnelVNC) Ping(_ context.Context, _ string) error {
return nil
}
// --- Local implementation ---
type localVNC struct {
sb sandbox.Sandbox
}
// NewLocal creates a VNC that resolves host VNC ports via sandbox.Inspect.
func NewLocal(sb sandbox.Sandbox) VNC {
return &localVNC{sb: sb}
}
func (l *localVNC) URL(ctx context.Context, containerID string) (string, error) {
info, err := l.sb.Inspect(ctx, containerID)
if err != nil {
return "", fmt.Errorf("inspect: %w", err)
}
for _, p := range info.Ports {
if p.ContainerPort == defaultVNCContainerPort && p.HostPort != 0 {
ip := p.HostIP
if ip == "" {
ip = "127.0.0.1"
}
return fmt.Sprintf("ws://%s:%d/ws", ip, p.HostPort), nil
}
}
return "", fmt.Errorf("VNC port %d not mapped for container %s", defaultVNCContainerPort, containerID)
}
func (l *localVNC) Ping(ctx context.Context, containerID string) error {
url, err := l.URL(ctx, containerID)
if err != nil {
return err
}
httpURL := "http" + url[2:]
req, err := http.NewRequestWithContext(ctx, http.MethodGet, httpURL, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
resp.Body.Close()
return nil
}