- 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
380 lines
9.6 KiB
Go
380 lines
9.6 KiB
Go
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 := ®istry.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,
|
||
}
|
||
}
|