yao/tai/tunnel/server.go
Max 83ebe49036 refactor: unify server lifecycle, migrate gRPC client, and clean up sandbox v2
Server lifecycle:
- Introduce service.Service to manage HTTP + gRPC startup/shutdown
- Fix gRPC mutex deadlock in StartServer when port is occupied
- Add GracefulStop with 5s timeout before forced Stop in grpc.go
- Pre-check HTTP and gRPC port availability in cmd/start.go
- Print gRPC server address in startup access-points block

gRPC client refactor:
- Move token manager and client from tai/grpc/ to grpc/client/
- Add backward-compatible aliases in tai/yao.go and tai/token.go
- Update cmd/run.go to import grpc/client directly (no tai dependency)

Sandbox v2 docker migration:
- Delete sandbox/v2/docker/ (moved to tai repo)
- Update sandbox/docker/build.sh hint to point to tai repo
- Clean up .gitignore entries for removed docker directory
- Temporarily disable SandboxV2Test and BenchmarkSandboxV2 in CI
  (docker images need rebuild after tai repo migration)

Tai integration:
- Add direct-mode registration API handlers in tai/api/
- Add heartbeat handler and token management wrappers
- Update tai/registry and tai/tunnel for latest protocol
- Replace yao-grpc references with tai call in docs

Made-with: Cursor
2026-03-07 17:19:19 +08:00

269 lines
6.8 KiB
Go

package tunnel
import (
"fmt"
"io"
"log/slog"
"net"
"net/http"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
oauth "github.com/yaoapp/yao/openapi/oauth"
"github.com/yaoapp/yao/tai/registry"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
// HandleControl handles the Tai control channel WebSocket: GET /ws/tai.
// Authenticates via Bearer token, reads register + ping messages,
// and maintains the Tai node in the global registry.
func HandleControl(c *gin.Context) {
logger := slog.Default()
reg := registry.Global()
if reg == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "registry not initialized"})
return
}
bearer := extractBearer(c.Request)
if bearer == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing authorization"})
return
}
authInfo, err := authenticateBearerFunc(bearer)
if err != nil {
logger.Warn("tunnel auth failed", "err", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication failed"})
return
}
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Error("ws upgrade failed", "err", err)
return
}
// Read the register message
var regMsg registerMessage
if err := conn.ReadJSON(&regMsg); err != nil {
logger.Error("read register message", "err", err)
conn.Close()
return
}
if regMsg.Type != "register" {
logger.Error("expected register message", "got", regMsg.Type)
conn.Close()
return
}
if regMsg.TaiID == "" {
logger.Error("register message missing tai_id")
conn.Close()
return
}
node := &registry.TaiNode{
TaiID: regMsg.TaiID,
MachineID: regMsg.MachineID,
Version: regMsg.Version,
Auth: authInfo,
System: regMsg.System,
Mode: "tunnel",
YaoBase: regMsg.Server,
Ports: regMsg.Ports,
Capabilities: regMsg.Capabilities,
ControlConn: conn,
}
reg.Register(node)
defer func() {
reg.Unregister(regMsg.TaiID)
logger.Info("tai tunnel disconnected", "tai_id", regMsg.TaiID)
}()
if err := reg.WriteControlJSON(regMsg.TaiID, map[string]string{"type": "registered", "tai_id": regMsg.TaiID}); err != nil {
logger.Error("write registered response", "err", err)
return
}
logger.Info("tai tunnel connected", "tai_id", regMsg.TaiID, "version", regMsg.Version)
for {
var msg controlMsg
if err := conn.ReadJSON(&msg); err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
logger.Debug("control channel read error", "err", err)
}
return
}
switch msg.Type {
case "ping":
reg.UpdatePing(regMsg.TaiID)
if err := reg.WriteControlJSON(regMsg.TaiID, map[string]string{"type": "pong"}); err != nil {
logger.Debug("pong write failed", "err", err)
return
}
default:
logger.Debug("unknown control message", "type", msg.Type)
}
}
}
// HandleData handles a Tai data channel WebSocket: GET /ws/tai/data/:channel_id.
// Authenticates via Bearer token, verifies the caller matches the pending
// channel's owner, then wraps the WS as a net.Conn for bidirectional bridging.
func HandleData(c *gin.Context) {
logger := slog.Default()
reg := registry.Global()
if reg == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "registry not initialized"})
return
}
bearer := extractBearer(c.Request)
if bearer == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing authorization"})
return
}
authInfo, err := authenticateBearerFunc(bearer)
if err != nil {
logger.Warn("data channel auth failed", "err", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication failed"})
return
}
channelID := c.Param("channel_id")
if channelID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "missing channel_id"})
return
}
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Error("ws data upgrade failed", "err", err)
return
}
wsConn := newWSConn(conn)
if err := reg.AcceptDataChannel(channelID, authInfo.ClientID, wsConn); err != nil {
logger.Debug("accept data channel failed", "channel_id", channelID, "err", err)
conn.Close()
return
}
}
// registerMessage is the JSON structure for Tai's register message.
type registerMessage struct {
Type string `json:"type"`
TaiID string `json:"tai_id"`
MachineID string `json:"machine_id"`
Version string `json:"version"`
Server string `json:"server"`
Ports map[string]int `json:"ports"`
Capabilities map[string]bool `json:"capabilities"`
System registry.SystemInfo `json:"system"`
}
// controlMsg is a generic control channel message.
type controlMsg struct {
Type string `json:"type"`
}
func extractBearer(r *http.Request) string {
auth := r.Header.Get("Authorization")
if len(auth) > 7 && strings.EqualFold(auth[:7], "bearer ") {
return auth[7:]
}
return ""
}
var authenticateBearerFunc = authenticateBearerDefault
func authenticateBearerDefault(token string) (registry.AuthInfo, error) {
svc := oauth.OAuth
if svc == nil {
return registry.AuthInfo{}, fmt.Errorf("oauth service not initialized")
}
result, err := svc.AuthenticateToken(oauth.AuthInput{
AccessToken: token,
})
if err != nil {
return registry.AuthInfo{}, err
}
info := registry.AuthInfo{}
if result.Info != nil {
info.Subject = result.Info.Subject
info.UserID = result.Info.UserID
info.ClientID = result.Info.ClientID
info.Scope = result.Info.Scope
info.TeamID = result.Info.TeamID
info.TenantID = result.Info.TenantID
}
return info, nil
}
// wsConn wraps a gorilla/websocket.Conn to implement net.Conn for raw byte bridging.
type wsConn struct {
ws *websocket.Conn
reader io.Reader
mu sync.Mutex
}
func newWSConn(ws *websocket.Conn) *wsConn {
return &wsConn{ws: ws}
}
func (c *wsConn) Read(p []byte) (int, error) {
for {
if c.reader != nil {
n, err := c.reader.Read(p)
if n > 0 {
return n, nil
}
c.reader = nil
if err != nil && err != io.EOF {
return 0, err
}
}
_, reader, err := c.ws.NextReader()
if err != nil {
return 0, err
}
c.reader = reader
}
}
func (c *wsConn) Write(p []byte) (int, error) {
c.mu.Lock()
defer c.mu.Unlock()
err := c.ws.WriteMessage(websocket.BinaryMessage, p)
if err != nil {
return 0, err
}
return len(p), nil
}
func (c *wsConn) Close() error {
return c.ws.Close()
}
func (c *wsConn) LocalAddr() net.Addr { return c.ws.LocalAddr() }
func (c *wsConn) RemoteAddr() net.Addr { return c.ws.RemoteAddr() }
func (c *wsConn) SetDeadline(t time.Time) error {
if err := c.ws.SetReadDeadline(t); err != nil {
return err
}
return c.ws.SetWriteDeadline(t)
}
func (c *wsConn) SetReadDeadline(t time.Time) error { return c.ws.SetReadDeadline(t) }
func (c *wsConn) SetWriteDeadline(t time.Time) error { return c.ws.SetWriteDeadline(t) }