yao/tai/tunnel/grpc_handler.go
Max b667ec310b feat(registry, grpc): introduce channel ID constants and logging enhancements
- Added constants for channel ID byte size and short length to improve clarity and maintainability.
- Implemented ShortChannelID function to truncate channel IDs for log messages, enhancing readability.
- Updated gRPC tunnel logging to utilize the new ShortChannelID function, ensuring consistent and concise channel ID representation in logs.

Made-with: Cursor
2026-03-13 18:41:15 +08:00

380 lines
9.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package tunnel
import (
"fmt"
"io"
"log/slog"
"net"
"sync"
"time"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
"github.com/yaoapp/yao/grpc/auth"
tai "github.com/yaoapp/yao/tai"
"github.com/yaoapp/yao/tai/registry"
"github.com/yaoapp/yao/tai/taiid"
"github.com/yaoapp/yao/tai/tunnel/taipb"
"github.com/yaoapp/yao/tai/types"
)
var globalHandler *TunnelHandler
// GlobalHandler returns the global TunnelHandler instance set by NewTunnelHandler.
func GlobalHandler() *TunnelHandler { return globalHandler }
// TunnelHandler implements the TaiTunnel gRPC service.
type TunnelHandler struct {
taipb.UnimplementedTaiTunnelServer
reg *registry.Registry
pending sync.Map // channel_id → chan taipb.TaiTunnel_ForwardServer
logger *slog.Logger
sendMu sync.Map // taiID → *sync.Mutex serializes Send on each Register stream
}
// NewTunnelHandler creates a TunnelHandler backed by the given registry.
// It also registers a bridge function so that OpenLocalListener uses
// gRPC Forward streams instead of WS data channels.
func NewTunnelHandler(reg *registry.Registry) *TunnelHandler {
h := &TunnelHandler{
reg: reg,
logger: slog.Default(),
}
reg.SetBridgeFunc(h.bridgeConn)
globalHandler = h
return h
}
// Register implements the control-plane stream (Tai → Yao).
func (h *TunnelHandler) Register(stream taipb.TaiTunnel_RegisterServer) error {
msg, err := stream.Recv()
if err != nil {
return fmt.Errorf("recv register: %w", err)
}
if msg.Type != "register" {
return fmt.Errorf("expected register, got %q", msg.Type)
}
if msg.NodeId == "" || msg.MachineId == "" {
return fmt.Errorf("register: node_id and machine_id required")
}
resolvedTaiID, err := taiid.Generate(msg.MachineId, msg.NodeId)
if err != nil {
return fmt.Errorf("taiid: %w", err)
}
authInfo := authInfoFromStream(stream)
remoteIP := ""
if p, ok := peer.FromContext(stream.Context()); ok {
if host, _, err := net.SplitHostPort(p.Addr.String()); err == nil {
remoteIP = host
}
}
node := &registry.TaiNode{
TaiID: resolvedTaiID,
MachineID: msg.MachineId,
Version: msg.Version,
DisplayName: msg.DisplayName,
Auth: authInfo,
System: systemFromProto(msg.System),
Mode: "tunnel",
Addr: "tunnel://" + remoteIP,
Ports: portsFromProto(msg.Ports),
Capabilities: capsFromProto(msg.Caps),
}
var mu sync.Mutex
h.sendMu.Store(resolvedTaiID, &mu)
h.reg.Register(node)
h.reg.SetRegisterStream(resolvedTaiID, stream)
defer func() {
h.sendMu.Delete(resolvedTaiID)
h.reg.Unregister(resolvedTaiID)
h.logger.Info("tai gRPC tunnel disconnected", "tai_id", resolvedTaiID)
}()
mu.Lock()
err = stream.Send(&taipb.TunnelControl{
Type: "registered",
TaiId: resolvedTaiID,
})
mu.Unlock()
if err != nil {
return fmt.Errorf("send registered: %w", err)
}
h.logger.Info("tai gRPC tunnel connected", "tai_id", resolvedTaiID, "version", msg.Version)
go h.connectTunnelNode(resolvedTaiID)
const pingTimeout = 90 * time.Second
recvCh := make(chan *taipb.TunnelControl)
errCh := make(chan error, 1)
go func() {
for {
ctrl, err := stream.Recv()
if err != nil {
errCh <- err
return
}
recvCh <- ctrl
}
}()
timer := time.NewTimer(pingTimeout)
defer timer.Stop()
for {
select {
case ctrl := <-recvCh:
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
timer.Reset(pingTimeout)
switch ctrl.Type {
case "ping":
h.reg.UpdatePing(resolvedTaiID)
mu.Lock()
sendErr := stream.Send(&taipb.TunnelControl{Type: "pong"})
mu.Unlock()
if sendErr != nil {
return sendErr
}
}
case err := <-errCh:
if err == io.EOF {
return nil
}
return err
case <-timer.C:
h.logger.Warn("tai ping timeout, closing tunnel", "tai_id", resolvedTaiID, "timeout", pingTimeout)
return fmt.Errorf("tai %s: ping timeout (%s)", resolvedTaiID, pingTimeout)
}
}
}
// Forward implements the data-plane stream (Tai → Yao).
func (h *TunnelHandler) Forward(stream taipb.TaiTunnel_ForwardServer) error {
md, ok := metadata.FromIncomingContext(stream.Context())
if !ok {
return fmt.Errorf("missing metadata")
}
vals := md.Get("channel_id")
if len(vals) == 0 || vals[0] == "" {
return fmt.Errorf("missing channel_id in metadata")
}
channelID := vals[0]
short := registry.ShortChannelID(channelID)
h.logger.Debug("[forward] Forward stream arrived", "channel_id", short)
if ch, ok := h.pending.LoadAndDelete(channelID); ok {
ch.(chan taipb.TaiTunnel_ForwardServer) <- stream
} else {
h.logger.Warn("[forward] no pending channel (expired?)", "channel_id", short)
return fmt.Errorf("no pending channel for %s", channelID)
}
<-stream.Context().Done()
return nil
}
// RequestForward sends an "open" command to Tai via the Register stream and
// waits for Tai to call back with a Forward stream. Returns the Forward stream.
func (h *TunnelHandler) RequestForward(taiID string, targetPort int) (taipb.TaiTunnel_ForwardServer, error) {
stream := h.reg.GetRegisterStream(taiID)
if stream == nil {
return nil, fmt.Errorf("tai %s: no active register stream", taiID)
}
muVal, ok := h.sendMu.Load(taiID)
if !ok {
return nil, fmt.Errorf("tai %s: no send mutex (stream closing?)", taiID)
}
mu := muVal.(*sync.Mutex)
channelID, err := registry.GenerateChannelID()
if err != nil {
return nil, fmt.Errorf("generate channel_id: %w", err)
}
waitCh := make(chan taipb.TaiTunnel_ForwardServer, 1)
h.pending.Store(channelID, waitCh)
defer h.pending.Delete(channelID)
regStream, ok := stream.(taipb.TaiTunnel_RegisterServer)
if !ok {
return nil, fmt.Errorf("tai %s: register stream type mismatch", taiID)
}
short := registry.ShortChannelID(channelID)
h.logger.Debug("[forward] sending open command",
"tai_id", taiID, "port", targetPort, "channel_id", short)
mu.Lock()
sendErr := regStream.Send(&taipb.TunnelControl{
Type: "open",
ChannelId: channelID,
TargetPort: int32(targetPort),
})
mu.Unlock()
if sendErr != nil {
return nil, fmt.Errorf("send open: %w", sendErr)
}
h.logger.Debug("[forward] open sent, waiting for callback",
"tai_id", taiID, "port", targetPort, "channel_id", short)
select {
case fwd := <-waitCh:
h.logger.Debug("[forward] callback received",
"tai_id", taiID, "channel_id", short)
return fwd, nil
case <-time.After(10 * time.Second):
return nil, fmt.Errorf("tai %s: forward timeout (10s) channel=%s", taiID, short)
case <-regStream.Context().Done():
return nil, fmt.Errorf("tai %s: register stream closed while waiting for forward", taiID)
}
}
// connectTunnelNode establishes gRPC resources to the Tai node through the tunnel.
func (h *TunnelHandler) connectTunnelNode(taiID string) {
res, err := tai.DialTunnel(taiID, h.reg)
if err != nil {
h.logger.Warn("failed to connect tunnel node",
"tai_id", taiID, "err", err)
return
}
h.reg.SetResources(taiID, res)
h.logger.Info("tunnel node resources connected", "tai_id", taiID)
}
// bridgeConn bridges a local TCP connection to a Tai port via gRPC Forward stream.
// Called by registry.OpenLocalListener for each accepted TCP connection.
func (h *TunnelHandler) bridgeConn(taiID string, targetPort int, localConn net.Conn) {
fwd, err := h.RequestForward(taiID, targetPort)
if err != nil {
localConn.Close()
h.logger.Error("request forward failed",
"tai_id", taiID, "port", targetPort, "err", err)
return
}
streamConn := newForwardConn(fwd)
bridgeTCP(localConn, streamConn)
}
// forwardConn wraps a Forward stream as a net.Conn-like reader/writer.
type forwardConn struct {
stream taipb.TaiTunnel_ForwardServer
buf []byte
}
func newForwardConn(stream taipb.TaiTunnel_ForwardServer) *forwardConn {
return &forwardConn{stream: stream}
}
func (c *forwardConn) Read(p []byte) (int, error) {
if len(c.buf) > 0 {
n := copy(p, c.buf)
c.buf = c.buf[n:]
return n, nil
}
msg, err := c.stream.Recv()
if err != nil {
return 0, err
}
n := copy(p, msg.Data)
if n < len(msg.Data) {
c.buf = msg.Data[n:]
}
return n, nil
}
func (c *forwardConn) Write(p []byte) (int, error) {
if err := c.stream.Send(&taipb.ForwardData{Data: p}); err != nil {
return 0, err
}
return len(p), nil
}
func (c *forwardConn) Close() error {
return nil
}
// bridgeTCP copies bytes bidirectionally, closing both sides when done.
func bridgeTCP(a, b io.ReadWriteCloser) {
var wg sync.WaitGroup
wg.Add(2)
cp := func(dst io.WriteCloser, src io.ReadCloser) {
defer wg.Done()
io.Copy(dst, src)
dst.Close()
}
go cp(a, b)
go cp(b, a)
wg.Wait()
}
// ── helpers ──────────────────────────────────────────────────────────────────
func authInfoFromStream(stream taipb.TaiTunnel_RegisterServer) types.AuthInfo {
info := auth.GetAuthorizedInfo(stream.Context())
if info == nil {
return types.AuthInfo{}
}
return types.AuthInfo{
Subject: info.Subject,
UserID: info.UserID,
ClientID: info.ClientID,
Scope: info.Scope,
TeamID: info.TeamID,
TenantID: info.TenantID,
}
}
func portsFromProto(p *taipb.Ports) types.Ports {
if p == nil {
return types.Ports{}
}
return types.Ports{
GRPC: int(p.Grpc),
HTTP: int(p.Http),
VNC: int(p.Vnc),
Docker: int(p.Docker),
K8s: int(p.K8S),
}
}
func capsFromProto(c *taipb.Capabilities) types.Capabilities {
if c == nil {
return types.Capabilities{}
}
return types.Capabilities{
Docker: c.Docker,
K8s: c.K8S,
HostExec: c.HostExec,
}
}
func systemFromProto(s *taipb.SystemInfo) types.SystemInfo {
if s == nil {
return types.SystemInfo{}
}
return types.SystemInfo{
OS: s.Os,
Arch: s.Arch,
Hostname: s.Hostname,
Shell: s.Shell,
}
}