feat(tunnel): refactor VNC and proxy handling with structured routing

- Updated VNC and proxy handling in the tunnel to utilize a structured routing approach, enhancing clarity and maintainability.
- Replaced direct port checks with a new `forwardRoute` struct to encapsulate routing information, including channel type, container ID, and port.
- Modified request handling to streamline the forwarding process and improve error handling for unknown routes.
- Enhanced tests to validate the new routing logic and ensure consistent behavior across VNC and proxy requests.

Made-with: Cursor
This commit is contained in:
Max 2026-03-14 21:51:32 +08:00
parent d26bbd1e1f
commit 9c9701ed7e
10 changed files with 379 additions and 245 deletions

View file

@ -203,7 +203,7 @@ func nodeToHostOption(s taitypes.NodeMeta) computerOption {
Status: status, Status: status,
Mode: s.Mode, Mode: s.Mode,
Addr: addr, Addr: addr,
VNC: s.Ports.VNC > 0, VNC: s.Capabilities.VNC,
System: computerSystemInfo{ System: computerSystemInfo{
OS: s.System.OS, OS: s.System.OS,
Arch: s.System.Arch, Arch: s.System.Arch,
@ -239,7 +239,7 @@ func nodeToNodeOption(s taitypes.NodeMeta) computerOption {
Status: status, Status: status,
Mode: s.Mode, Mode: s.Mode,
Addr: addr, Addr: addr,
VNC: s.Ports.VNC > 0, VNC: s.Capabilities.VNC,
System: computerSystemInfo{ System: computerSystemInfo{
OS: s.System.OS, OS: s.System.OS,
Arch: s.System.Arch, Arch: s.System.Arch,

View file

@ -197,7 +197,7 @@ func hostToResponse(s taitypes.NodeMeta) sandboxResponse {
Policy: "persistent", Policy: "persistent",
Mode: s.Mode, Mode: s.Mode,
Addr: addr, Addr: addr,
VNC: s.Ports.VNC > 0, VNC: s.Capabilities.VNC,
CreatedAt: s.ConnectedAt, CreatedAt: s.ConnectedAt,
LastActive: s.LastPing, LastActive: s.LastPing,
System: sandboxSystemInfo{ System: sandboxSystemInfo{

View file

@ -294,6 +294,7 @@ func capsFromMap(m map[string]bool) types.Capabilities {
Docker: m["docker"], Docker: m["docker"],
K8s: m["k8s"], K8s: m["k8s"],
HostExec: m["host_exec"], HostExec: m["host_exec"],
VNC: m["vnc"],
} }
} }

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"strconv"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -12,11 +13,24 @@ import (
"github.com/yaoapp/yao/tai/types" "github.com/yaoapp/yao/tai/types"
) )
const defaultVNCPort = 5900
// forwardRoute holds the structured routing information extracted from the
// incoming request URL. It is passed to RequestForward so that Yao can
// populate the TunnelControl proto fields and Tai can route directly without
// parsing the first packet.
type forwardRoute struct {
channelType string // "proxy" | "vnc"
containerID string // target container or "__host__"
containerPort int // container-internal port (vnc default 5900)
subpath string // rewritten request path for the container
}
// HandleForward handles HTTP/VNC/any TCP-level forwarding through the gRPC tunnel. // HandleForward handles HTTP/VNC/any TCP-level forwarding through the gRPC tunnel.
// Route: ANY /tai/:taiID/proxy/*path and GET /tai/:taiID/vnc/*path // Route: ANY /tai/:taiID/proxy/*path and GET /tai/:taiID/vnc/*path
// //
// It hijacks the browser's raw TCP connection, asks Tai to open a Forward stream // It hijacks the browser's raw TCP connection, asks Tai to open a Forward stream
// to the resolved target port, rewrites the request path, and then performs // with explicit routing information, rewrites the request path, and then performs
// bidirectional byte-level bridging. No protocol parsing beyond HTTP hijack. // bidirectional byte-level bridging. No protocol parsing beyond HTTP hijack.
func (h *TunnelHandler) HandleForward(c *gin.Context) { func (h *TunnelHandler) HandleForward(c *gin.Context) {
logger := h.logger logger := h.logger
@ -30,16 +44,18 @@ func (h *TunnelHandler) HandleForward(c *gin.Context) {
return return
} }
targetPort := resolveTargetPort(c, node) route, err := resolveRoute(c, node)
if targetPort == 0 { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "cannot resolve target port"}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
rewrittenReq := rewriteRequest(c.Request, taiID) rewrittenReq := rewriteRequest(c.Request, taiID, route)
logger.Debug("[forward] "+node.Mode+" → tai:"+fmt.Sprintf("%d", targetPort), logger.Debug("[forward] "+node.Mode+" → tai",
"tai_id", taiID, "tai_id", taiID,
"addr", node.Addr, "type", route.channelType,
"container", route.containerID,
"container_port", route.containerPort,
"path", rewrittenReq.URL.Path, "path", rewrittenReq.URL.Path,
) )
@ -55,10 +71,10 @@ func (h *TunnelHandler) HandleForward(c *gin.Context) {
} }
defer browserConn.Close() defer browserConn.Close()
fwd, err := h.RequestForward(taiID, targetPort) fwd, err := h.RequestForward(taiID, route)
if err != nil { if err != nil {
logger.Error("[forward] stream failed", logger.Error("[forward] stream failed",
"tai_id", taiID, "port", targetPort, "err", err) "tai_id", taiID, "type", route.channelType, "err", err)
browserConn.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n")) browserConn.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n"))
return return
} }
@ -94,47 +110,91 @@ func HandleForwardLazy(c *gin.Context) {
h.HandleForward(c) h.HandleForward(c)
} }
// resolveTargetPort determines the Tai-side port from the route pattern. // resolveRoute extracts structured routing info from the request URL path.
func resolveTargetPort(c *gin.Context, node *types.NodeMeta) int { //
// For proxy requests (/tai/:taiID/proxy/{containerID}:{port}/{subpath}):
//
// channelType = "proxy", containerPort from URL, subpath = remaining path.
//
// For VNC requests (/tai/:taiID/vnc/{containerID}/ws):
//
// channelType = "vnc", containerPort = 5900, subpath = /vnc/{containerID}/ws.
func resolveRoute(c *gin.Context, node *types.NodeMeta) (*forwardRoute, error) {
path := c.Request.URL.Path path := c.Request.URL.Path
taiID := c.Param("taiID")
if strings.Contains(path, "/vnc/") {
if node.Ports.VNC != 0 {
return node.Ports.VNC
}
return 16080
}
if strings.Contains(path, "/proxy/") {
if node.Ports.HTTP != 0 {
return node.Ports.HTTP
}
return 8099
}
return 0
}
// rewriteRequest clones the request and strips the Yao-side route prefix,
// leaving only what the Tai-side handler expects.
//
// The Tai httpproxy expects /{containerID}:{port}/..., so the /proxy prefix
// is stripped. The Tai VNC router expects /vnc/{containerID}/ws, so the /vnc
// prefix is kept.
//
// /v1/tai/abc/proxy/cid:8080/foo → /cid:8080/foo
// /v1/tai/abc/vnc/cid/ws → /vnc/cid/ws
func rewriteRequest(orig *http.Request, taiID string) *http.Request {
r := orig.Clone(orig.Context())
marker := "/tai/" + taiID marker := "/tai/" + taiID
if idx := strings.Index(r.URL.Path, marker); idx >= 0 { idx := strings.Index(path, marker)
rest := r.URL.Path[idx+len(marker):] if idx < 0 {
rest = strings.TrimPrefix(rest, "/proxy") return nil, fmt.Errorf("cannot locate /tai/%s in path", taiID)
if rest == "" { }
rest = "/" rest := path[idx+len(marker):]
if strings.HasPrefix(rest, "/vnc/") {
// /vnc/{containerID}/ws → containerID, port=5900
tail := strings.TrimPrefix(rest, "/vnc/")
containerID := tail
if slashIdx := strings.IndexByte(tail, '/'); slashIdx >= 0 {
containerID = tail[:slashIdx]
} }
r.URL.Path = rest if containerID == "" {
return nil, fmt.Errorf("missing container ID in VNC path: %s", path)
}
return &forwardRoute{
channelType: "vnc",
containerID: containerID,
containerPort: defaultVNCPort,
subpath: rest, // keep /vnc/{containerID}/ws
}, nil
} }
if strings.HasPrefix(rest, "/proxy/") {
// /proxy/{containerID}:{port}/{subpath}
proxyPath := strings.TrimPrefix(rest, "/proxy")
// proxyPath = /{containerID}:{port}/{subpath}
proxyPath = strings.TrimPrefix(proxyPath, "/")
if proxyPath == "" {
return nil, fmt.Errorf("empty proxy path")
}
slash := strings.IndexByte(proxyPath, '/')
var head, subpath string
if slash == -1 {
head = proxyPath
subpath = "/"
} else {
head = proxyPath[:slash]
subpath = proxyPath[slash:]
}
colon := strings.LastIndexByte(head, ':')
if colon < 0 {
return nil, fmt.Errorf("missing port in proxy path: %s", path)
}
containerID := head[:colon]
portStr := head[colon+1:]
port, err := strconv.Atoi(portStr)
if err != nil {
return nil, fmt.Errorf("invalid port %q in proxy path: %w", portStr, err)
}
return &forwardRoute{
channelType: "proxy",
containerID: containerID,
containerPort: port,
subpath: subpath,
}, nil
}
return nil, fmt.Errorf("unknown route pattern: %s", rest)
}
// rewriteRequest clones the request and sets the path to the route's subpath.
//
// For proxy: the path becomes the subpath (e.g. /foo/bar).
// For VNC: the path keeps /vnc/{containerID}/ws as-is.
func rewriteRequest(orig *http.Request, taiID string, route *forwardRoute) *http.Request {
r := orig.Clone(orig.Context())
r.URL.Path = route.subpath
r.RequestURI = r.URL.RequestURI() r.RequestURI = r.URL.RequestURI()
return r return r
} }

View file

@ -16,155 +16,131 @@ func init() {
gin.SetMode(gin.TestMode) gin.SetMode(gin.TestMode)
} }
func TestResolveTargetPort_VNC(t *testing.T) { func TestResolveRoute_Proxy(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
path string path string
vncPort int wantType string
wantPort int wantContainer string
wantPort int
wantSubpath string
}{ }{
{"default_vnc", "/tai/abc/vnc/websockify", 0, 16080}, {
{"custom_vnc", "/tai/abc/vnc/websockify", 5900, 5900}, "basic_proxy",
"/tai/abc/proxy/cid123:8080/foo/bar",
"proxy", "cid123", 8080, "/foo/bar",
},
{
"proxy_root",
"/tai/abc/proxy/cid:3000",
"proxy", "cid", 3000, "/",
},
{
"proxy_host",
"/v1/tai/abc/proxy/__host__:9090/api",
"proxy", "__host__", 9090, "/api",
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c, _ := gin.CreateTestContext(httptest.NewRecorder()) c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = &http.Request{URL: &url.URL{Path: tt.path}} c.Request = &http.Request{URL: &url.URL{Path: tt.path}}
node := &types.NodeMeta{Ports: types.Ports{VNC: tt.vncPort}} c.Params = gin.Params{{Key: "taiID", Value: "abc"}}
got := resolveTargetPort(c, node) node := &types.NodeMeta{}
if got != tt.wantPort {
t.Errorf("resolveTargetPort = %d, want %d", got, tt.wantPort) r, err := resolveRoute(c, node)
if err != nil {
t.Fatalf("resolveRoute error: %v", err)
}
if r.channelType != tt.wantType {
t.Errorf("channelType = %q, want %q", r.channelType, tt.wantType)
}
if r.containerID != tt.wantContainer {
t.Errorf("containerID = %q, want %q", r.containerID, tt.wantContainer)
}
if r.containerPort != tt.wantPort {
t.Errorf("containerPort = %d, want %d", r.containerPort, tt.wantPort)
}
if r.subpath != tt.wantSubpath {
t.Errorf("subpath = %q, want %q", r.subpath, tt.wantSubpath)
} }
}) })
} }
} }
func TestResolveTargetPort_Proxy(t *testing.T) { func TestResolveRoute_VNC(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
path string path string
httpPort int wantContainer string
wantPort int wantPort int
}{ }{
{"default_proxy", "/tai/abc/proxy/api/v1/foo", 0, 8099}, {"vnc_basic", "/tai/abc/vnc/container1/ws", "container1", defaultVNCPort},
{"custom_proxy", "/tai/abc/proxy/api/v1/foo", 9090, 9090}, {"vnc_host", "/v1/tai/abc/vnc/__host__/ws", "__host__", defaultVNCPort},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c, _ := gin.CreateTestContext(httptest.NewRecorder()) c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = &http.Request{URL: &url.URL{Path: tt.path}} c.Request = &http.Request{URL: &url.URL{Path: tt.path}}
node := &types.NodeMeta{Ports: types.Ports{HTTP: tt.httpPort}} c.Params = gin.Params{{Key: "taiID", Value: "abc"}}
got := resolveTargetPort(c, node) node := &types.NodeMeta{}
if got != tt.wantPort {
t.Errorf("resolveTargetPort = %d, want %d", got, tt.wantPort) r, err := resolveRoute(c, node)
if err != nil {
t.Fatalf("resolveRoute error: %v", err)
}
if r.channelType != "vnc" {
t.Errorf("channelType = %q, want vnc", r.channelType)
}
if r.containerID != tt.wantContainer {
t.Errorf("containerID = %q, want %q", r.containerID, tt.wantContainer)
}
if r.containerPort != tt.wantPort {
t.Errorf("containerPort = %d, want %d", r.containerPort, tt.wantPort)
} }
}) })
} }
} }
func TestResolveTargetPort_Unknown(t *testing.T) { func TestResolveRoute_Unknown(t *testing.T) {
c, _ := gin.CreateTestContext(httptest.NewRecorder()) c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = &http.Request{URL: &url.URL{Path: "/tai/abc/unknown/something"}} c.Request = &http.Request{URL: &url.URL{Path: "/tai/abc/unknown/something"}}
c.Params = gin.Params{{Key: "taiID", Value: "abc"}}
node := &types.NodeMeta{} node := &types.NodeMeta{}
got := resolveTargetPort(c, node)
if got != 0 { _, err := resolveRoute(c, node)
t.Errorf("resolveTargetPort = %d, want 0", got) if err == nil {
t.Error("expected error for unknown route")
} }
} }
func TestRewriteRequest(t *testing.T) { func TestRewriteRequest_Proxy(t *testing.T) {
tests := []struct { u, _ := url.Parse("http://localhost/v1/tai/abc/proxy/cid:8080/foo")
name string orig := &http.Request{
origPath string Method: "GET",
taiID string URL: u,
wantPath string RequestURI: u.RequestURI(),
wantURI string Host: "localhost",
}{ Header: http.Header{},
{ }
"proxy_path", route := &forwardRoute{
"/tai/abc123/proxy/api/v1/data", channelType: "proxy",
"abc123", containerID: "cid",
"/api/v1/data", containerPort: 8080,
"/api/v1/data", subpath: "/foo",
},
{
"vnc_path",
"/tai/node-1/vnc/websockify",
"node-1",
"/vnc/websockify",
"/vnc/websockify",
},
{
"with_query",
"/tai/node-1/proxy/api?foo=bar",
"node-1",
"/api",
"/api?foo=bar",
},
{
"exact_prefix",
"/tai/node-1",
"node-1",
"/",
"/",
},
{
"with_base_url",
"/v1/tai/node-1/proxy/api/v1/data",
"node-1",
"/api/v1/data",
"/api/v1/data",
},
{
"with_base_url_vnc",
"/v1/tai/abc123/vnc/__host__/ws",
"abc123",
"/vnc/__host__/ws",
"/vnc/__host__/ws",
},
{
"proxy_container_port",
"/v1/tai/abc/proxy/cid123:8080/foo",
"abc",
"/cid123:8080/foo",
"/cid123:8080/foo",
},
{
"no_match",
"/other/path",
"node-1",
"/other/path",
"/other/path",
},
} }
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
u, _ := url.Parse("http://localhost" + tt.origPath)
orig := &http.Request{
Method: "GET",
URL: u,
RequestURI: u.RequestURI(),
Host: "localhost",
Header: http.Header{},
}
got := rewriteRequest(orig, tt.taiID) got := rewriteRequest(orig, "abc", route)
if got.URL.Path != "/foo" {
if got.URL.Path != tt.wantPath { t.Errorf("path = %q, want /foo", got.URL.Path)
t.Errorf("path = %q, want %q", got.URL.Path, tt.wantPath) }
} if got == orig {
if got.RequestURI != tt.wantURI { t.Error("rewriteRequest should return a clone")
t.Errorf("requestURI = %q, want %q", got.RequestURI, tt.wantURI)
}
if got == orig {
t.Error("rewriteRequest should return a clone, not the original")
}
})
} }
} }
func TestRewriteRequest_PreservesHeaders(t *testing.T) { func TestRewriteRequest_VNC(t *testing.T) {
u, _ := url.Parse("http://localhost/tai/node-1/vnc/websockify") u, _ := url.Parse("http://localhost/tai/node-1/vnc/cid/ws")
orig := &http.Request{ orig := &http.Request{
Method: "GET", Method: "GET",
URL: u, URL: u,
@ -175,14 +151,20 @@ func TestRewriteRequest_PreservesHeaders(t *testing.T) {
"Upgrade": {"websocket"}, "Upgrade": {"websocket"},
}, },
} }
route := &forwardRoute{
channelType: "vnc",
containerID: "cid",
containerPort: 5900,
subpath: "/vnc/cid/ws",
}
got := rewriteRequest(orig, "node-1") got := rewriteRequest(orig, "node-1", route)
if got.URL.Path != "/vnc/cid/ws" {
t.Errorf("path = %q, want /vnc/cid/ws", got.URL.Path)
}
if got.Header.Get("Connection") != "Upgrade" { if got.Header.Get("Connection") != "Upgrade" {
t.Error("expected Connection header preserved") t.Error("expected Connection header preserved")
} }
if got.Header.Get("Upgrade") != "websocket" {
t.Error("expected Upgrade header preserved")
}
} }
func TestHandleForwardLazy_NilHandler(t *testing.T) { func TestHandleForwardLazy_NilHandler(t *testing.T) {
@ -217,29 +199,24 @@ func TestHandleForward_NodeNotFound(t *testing.T) {
} }
} }
func TestHandleForward_NodeOffline(t *testing.T) { func TestHandleForward_UnknownRoute(t *testing.T) {
reg := registry.NewForTest() reg := registry.NewForTest()
h := NewTunnelHandler(reg) h := NewTunnelHandler(reg)
reg.Register(&registry.TaiNode{ reg.Register(&registry.TaiNode{
TaiID: "offline-node", TaiID: "online-node",
Mode: "tunnel", Mode: "tunnel",
Ports: types.Ports{HTTP: 8099},
}) })
// Manually set status to offline via a Get() — the node is online by default
// after Register, but we need an offline one. We'll use Unregister + re-register
// pattern. Actually, let's just test with a node that doesn't exist:
// the NodeNotFound test above covers that case. Instead, test zero port.
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w) c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/tai/offline-node/unknown/foo", nil) c.Request = httptest.NewRequest("GET", "/tai/online-node/unknown/foo", nil)
c.Params = gin.Params{{Key: "taiID", Value: "offline-node"}} c.Params = gin.Params{{Key: "taiID", Value: "online-node"}}
h.HandleForward(c) h.HandleForward(c)
if w.Code != http.StatusBadRequest { if w.Code != http.StatusBadRequest {
t.Errorf("expected 400 for unresolvable port, got %d", w.Code) t.Errorf("expected 400 for unresolvable route, got %d", w.Code)
} }
} }
@ -268,7 +245,6 @@ func TestHandleForward_ViaRealHTTP(t *testing.T) {
reg.Register(&registry.TaiNode{ reg.Register(&registry.TaiNode{
TaiID: "http-node", TaiID: "http-node",
Mode: "tunnel", Mode: "tunnel",
Ports: types.Ports{HTTP: 8099},
}) })
router := gin.New() router := gin.New()
@ -277,16 +253,12 @@ func TestHandleForward_ViaRealHTTP(t *testing.T) {
srv := httptest.NewServer(router) srv := httptest.NewServer(router)
defer srv.Close() defer srv.Close()
resp, err := http.Get(srv.URL + "/tai/http-node/proxy/api") resp, err := http.Get(srv.URL + "/tai/http-node/proxy/cid:8080/api")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer resp.Body.Close() defer resp.Body.Close()
// RequestForward will fail (no register stream) → hijacked conn gets "502"
// or the response will be a 502 written before hijack.
// Since hijack happens, the actual HTTP status may not be set normally.
// We just verify no panic and the request completes.
if resp.StatusCode == 200 { if resp.StatusCode == 200 {
t.Error("expected non-200 response for failed forward") t.Error("expected non-200 response for failed forward")
} }

View file

@ -191,7 +191,9 @@ func (h *TunnelHandler) Forward(stream taipb.TaiTunnel_ForwardServer) error {
// RequestForward sends an "open" command to Tai via the Register stream and // 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. // 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) { //
// route may be nil for raw TCP tunnels (gRPC, Docker API, K8s API).
func (h *TunnelHandler) RequestForward(taiID string, route *forwardRoute) (taipb.TaiTunnel_ForwardServer, error) {
stream := h.reg.GetRegisterStream(taiID) stream := h.reg.GetRegisterStream(taiID)
if stream == nil { if stream == nil {
return nil, fmt.Errorf("tai %s: no active register stream", taiID) return nil, fmt.Errorf("tai %s: no active register stream", taiID)
@ -217,23 +219,30 @@ func (h *TunnelHandler) RequestForward(taiID string, targetPort int) (taipb.TaiT
return nil, fmt.Errorf("tai %s: register stream type mismatch", taiID) return nil, fmt.Errorf("tai %s: register stream type mismatch", taiID)
} }
ctrl := &taipb.TunnelControl{
Type: "open",
ChannelId: channelID,
}
if route != nil {
ctrl.ChannelType = route.channelType
ctrl.ContainerId = route.containerID
ctrl.ContainerPort = int32(route.containerPort)
}
short := registry.ShortChannelID(channelID) short := registry.ShortChannelID(channelID)
h.logger.Debug("[forward] sending open command", h.logger.Debug("[forward] sending open command",
"tai_id", taiID, "port", targetPort, "channel_id", short) "tai_id", taiID, "channel_type", ctrl.ChannelType,
"container", ctrl.ContainerId, "channel_id", short)
mu.Lock() mu.Lock()
sendErr := regStream.Send(&taipb.TunnelControl{ sendErr := regStream.Send(ctrl)
Type: "open",
ChannelId: channelID,
TargetPort: int32(targetPort),
})
mu.Unlock() mu.Unlock()
if sendErr != nil { if sendErr != nil {
return nil, fmt.Errorf("send open: %w", sendErr) return nil, fmt.Errorf("send open: %w", sendErr)
} }
h.logger.Debug("[forward] open sent, waiting for callback", h.logger.Debug("[forward] open sent, waiting for callback",
"tai_id", taiID, "port", targetPort, "channel_id", short) "tai_id", taiID, "channel_id", short)
select { select {
case fwd := <-waitCh: case fwd := <-waitCh:
@ -261,8 +270,9 @@ func (h *TunnelHandler) connectTunnelNode(taiID string) {
// bridgeConn bridges a local TCP connection to a Tai port via gRPC Forward stream. // bridgeConn bridges a local TCP connection to a Tai port via gRPC Forward stream.
// Called by registry.OpenLocalListener for each accepted TCP connection. // Called by registry.OpenLocalListener for each accepted TCP connection.
// Uses raw TCP forwarding (TargetPort only, no container routing).
func (h *TunnelHandler) bridgeConn(taiID string, targetPort int, localConn net.Conn) { func (h *TunnelHandler) bridgeConn(taiID string, targetPort int, localConn net.Conn) {
fwd, err := h.RequestForward(taiID, targetPort) fwd, err := h.requestForwardRaw(taiID, targetPort)
if err != nil { if err != nil {
localConn.Close() localConn.Close()
h.logger.Error("request forward failed", h.logger.Error("request forward failed",
@ -274,6 +284,59 @@ func (h *TunnelHandler) bridgeConn(taiID string, targetPort int, localConn net.C
bridgeTCP(localConn, streamConn) bridgeTCP(localConn, streamConn)
} }
// requestForwardRaw sends an "open" command with only TargetPort (no container
// routing). Used by bridgeConn for raw TCP tunnels (gRPC, Docker API, K8s API).
func (h *TunnelHandler) requestForwardRaw(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 (raw)",
"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)
}
select {
case fwd := <-waitCh:
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)
}
}
// forwardConn wraps a Forward stream as a net.Conn-like reader/writer. // forwardConn wraps a Forward stream as a net.Conn-like reader/writer.
type forwardConn struct { type forwardConn struct {
stream taipb.TaiTunnel_ForwardServer stream taipb.TaiTunnel_ForwardServer
@ -364,6 +427,7 @@ func capsFromProto(c *taipb.Capabilities) types.Capabilities {
Docker: c.Docker, Docker: c.Docker,
K8s: c.K8S, K8s: c.K8S,
HostExec: c.HostExec, HostExec: c.HostExec,
VNC: c.Vnc,
} }
} }

View file

@ -245,7 +245,7 @@ func TestRequestForward_NoRegisterStream(t *testing.T) {
reg.Register(&registry.TaiNode{TaiID: "no-stream", Mode: "tunnel"}) reg.Register(&registry.TaiNode{TaiID: "no-stream", Mode: "tunnel"})
_, err := h.RequestForward("no-stream", 8099) _, err := h.requestForwardRaw("no-stream", 8099)
if err == nil { if err == nil {
t.Fatal("expected error when no register stream") t.Fatal("expected error when no register stream")
} }
@ -258,7 +258,7 @@ func TestRequestForward_TypeMismatch(t *testing.T) {
reg.Register(&registry.TaiNode{TaiID: "bad-type", Mode: "tunnel"}) reg.Register(&registry.TaiNode{TaiID: "bad-type", Mode: "tunnel"})
reg.SetRegisterStream("bad-type", "not-a-stream") reg.SetRegisterStream("bad-type", "not-a-stream")
_, err := h.RequestForward("bad-type", 8099) _, err := h.requestForwardRaw("bad-type", 8099)
if err == nil { if err == nil {
t.Fatal("expected error for type mismatch") t.Fatal("expected error for type mismatch")
} }
@ -348,7 +348,7 @@ drainLoop:
requestDone.Add(1) requestDone.Add(1)
go func() { go func() {
defer requestDone.Done() defer requestDone.Done()
requestResult, requestErr = h.RequestForward(taiID, 8099) requestResult, requestErr = h.requestForwardRaw(taiID, 8099)
}() }()
// Receive the "open" command // Receive the "open" command
@ -748,7 +748,7 @@ func TestRequestForward_Timeout(t *testing.T) {
// by never sending Forward). We'll use a short context cancel to avoid waiting. // by never sending Forward). We'll use a short context cancel to avoid waiting.
done := make(chan error, 1) done := make(chan error, 1)
go func() { go func() {
_, err := h.RequestForward(taiID, 8099) _, err := h.requestForwardRaw(taiID, 8099)
done <- err done <- err
}() }()
@ -828,7 +828,7 @@ drained:
for i := 0; i < N; i++ { for i := 0; i < N; i++ {
port := 8099 + i port := 8099 + i
go func(port int) { go func(port int) {
_, err := h.RequestForward(taiID, port) _, err := h.requestForwardRaw(taiID, port)
results <- err results <- err
}(port) }(port)
} }
@ -926,7 +926,7 @@ drained2:
// Start RequestForward // Start RequestForward
fwdResult := make(chan error, 1) fwdResult := make(chan error, 1)
go func() { go func() {
_, err := h.RequestForward(taiID, 8099) _, err := h.requestForwardRaw(taiID, 8099)
fwdResult <- err fwdResult <- err
}() }()
@ -1101,7 +1101,7 @@ proxyDrained:
}() }()
// Now do an actual RequestForward + simulate browser side // Now do an actual RequestForward + simulate browser side
fwd, err := h.RequestForward(taiID, 8099) fwd, err := h.requestForwardRaw(taiID, 8099)
if err != nil { if err != nil {
t.Fatal("RequestForward:", err) t.Fatal("RequestForward:", err)
} }
@ -1267,7 +1267,7 @@ vncDrained:
}() }()
// Send WS upgrade request through tunnel // Send WS upgrade request through tunnel
fwd, err := h.RequestForward(taiID, 16080) fwd, err := h.requestForwardRaw(taiID, 16080)
if err != nil { if err != nil {
t.Fatal("RequestForward:", err) t.Fatal("RequestForward:", err)
} }

View file

@ -2,7 +2,7 @@
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.11
// protoc v4.25.0 // protoc v4.25.0
// source: tunnel.proto // source: tunnel/proto/tunnel.proto
package taipb package taipb
@ -33,8 +33,11 @@ type TunnelControl struct {
Caps *Capabilities `protobuf:"bytes,7,opt,name=caps,proto3" json:"caps,omitempty"` Caps *Capabilities `protobuf:"bytes,7,opt,name=caps,proto3" json:"caps,omitempty"`
System *SystemInfo `protobuf:"bytes,8,opt,name=system,proto3" json:"system,omitempty"` System *SystemInfo `protobuf:"bytes,8,opt,name=system,proto3" json:"system,omitempty"`
// Carried on "open" (Yao → Tai) // Carried on "open" (Yao → Tai)
ChannelId string `protobuf:"bytes,10,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` ChannelId string `protobuf:"bytes,10,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"`
TargetPort int32 `protobuf:"varint,11,opt,name=target_port,json=targetPort,proto3" json:"target_port,omitempty"` TargetPort int32 `protobuf:"varint,11,opt,name=target_port,json=targetPort,proto3" json:"target_port,omitempty"`
ChannelType string `protobuf:"bytes,12,opt,name=channel_type,json=channelType,proto3" json:"channel_type,omitempty"` // "proxy" | "vnc" | "" (legacy/raw TCP)
ContainerId string `protobuf:"bytes,13,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` // target container or "__host__"
ContainerPort int32 `protobuf:"varint,14,opt,name=container_port,json=containerPort,proto3" json:"container_port,omitempty"` // container-internal port (vnc default 5900)
// Carried on "registered" (Yao → Tai) // Carried on "registered" (Yao → Tai)
TaiId string `protobuf:"bytes,20,opt,name=tai_id,json=taiId,proto3" json:"tai_id,omitempty"` TaiId string `protobuf:"bytes,20,opt,name=tai_id,json=taiId,proto3" json:"tai_id,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
@ -43,7 +46,7 @@ type TunnelControl struct {
func (x *TunnelControl) Reset() { func (x *TunnelControl) Reset() {
*x = TunnelControl{} *x = TunnelControl{}
mi := &file_tunnel_proto_msgTypes[0] mi := &file_tunnel_proto_tunnel_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -55,7 +58,7 @@ func (x *TunnelControl) String() string {
func (*TunnelControl) ProtoMessage() {} func (*TunnelControl) ProtoMessage() {}
func (x *TunnelControl) ProtoReflect() protoreflect.Message { func (x *TunnelControl) ProtoReflect() protoreflect.Message {
mi := &file_tunnel_proto_msgTypes[0] mi := &file_tunnel_proto_tunnel_proto_msgTypes[0]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -68,7 +71,7 @@ func (x *TunnelControl) ProtoReflect() protoreflect.Message {
// Deprecated: Use TunnelControl.ProtoReflect.Descriptor instead. // Deprecated: Use TunnelControl.ProtoReflect.Descriptor instead.
func (*TunnelControl) Descriptor() ([]byte, []int) { func (*TunnelControl) Descriptor() ([]byte, []int) {
return file_tunnel_proto_rawDescGZIP(), []int{0} return file_tunnel_proto_tunnel_proto_rawDescGZIP(), []int{0}
} }
func (x *TunnelControl) GetType() string { func (x *TunnelControl) GetType() string {
@ -141,6 +144,27 @@ func (x *TunnelControl) GetTargetPort() int32 {
return 0 return 0
} }
func (x *TunnelControl) GetChannelType() string {
if x != nil {
return x.ChannelType
}
return ""
}
func (x *TunnelControl) GetContainerId() string {
if x != nil {
return x.ContainerId
}
return ""
}
func (x *TunnelControl) GetContainerPort() int32 {
if x != nil {
return x.ContainerPort
}
return 0
}
func (x *TunnelControl) GetTaiId() string { func (x *TunnelControl) GetTaiId() string {
if x != nil { if x != nil {
return x.TaiId return x.TaiId
@ -157,7 +181,7 @@ type ForwardData struct {
func (x *ForwardData) Reset() { func (x *ForwardData) Reset() {
*x = ForwardData{} *x = ForwardData{}
mi := &file_tunnel_proto_msgTypes[1] mi := &file_tunnel_proto_tunnel_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -169,7 +193,7 @@ func (x *ForwardData) String() string {
func (*ForwardData) ProtoMessage() {} func (*ForwardData) ProtoMessage() {}
func (x *ForwardData) ProtoReflect() protoreflect.Message { func (x *ForwardData) ProtoReflect() protoreflect.Message {
mi := &file_tunnel_proto_msgTypes[1] mi := &file_tunnel_proto_tunnel_proto_msgTypes[1]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -182,7 +206,7 @@ func (x *ForwardData) ProtoReflect() protoreflect.Message {
// Deprecated: Use ForwardData.ProtoReflect.Descriptor instead. // Deprecated: Use ForwardData.ProtoReflect.Descriptor instead.
func (*ForwardData) Descriptor() ([]byte, []int) { func (*ForwardData) Descriptor() ([]byte, []int) {
return file_tunnel_proto_rawDescGZIP(), []int{1} return file_tunnel_proto_tunnel_proto_rawDescGZIP(), []int{1}
} }
func (x *ForwardData) GetData() []byte { func (x *ForwardData) GetData() []byte {
@ -205,7 +229,7 @@ type Ports struct {
func (x *Ports) Reset() { func (x *Ports) Reset() {
*x = Ports{} *x = Ports{}
mi := &file_tunnel_proto_msgTypes[2] mi := &file_tunnel_proto_tunnel_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -217,7 +241,7 @@ func (x *Ports) String() string {
func (*Ports) ProtoMessage() {} func (*Ports) ProtoMessage() {}
func (x *Ports) ProtoReflect() protoreflect.Message { func (x *Ports) ProtoReflect() protoreflect.Message {
mi := &file_tunnel_proto_msgTypes[2] mi := &file_tunnel_proto_tunnel_proto_msgTypes[2]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -230,7 +254,7 @@ func (x *Ports) ProtoReflect() protoreflect.Message {
// Deprecated: Use Ports.ProtoReflect.Descriptor instead. // Deprecated: Use Ports.ProtoReflect.Descriptor instead.
func (*Ports) Descriptor() ([]byte, []int) { func (*Ports) Descriptor() ([]byte, []int) {
return file_tunnel_proto_rawDescGZIP(), []int{2} return file_tunnel_proto_tunnel_proto_rawDescGZIP(), []int{2}
} }
func (x *Ports) GetGrpc() int32 { func (x *Ports) GetGrpc() int32 {
@ -273,13 +297,14 @@ type Capabilities struct {
Docker bool `protobuf:"varint,1,opt,name=docker,proto3" json:"docker,omitempty"` Docker bool `protobuf:"varint,1,opt,name=docker,proto3" json:"docker,omitempty"`
K8S bool `protobuf:"varint,2,opt,name=k8s,proto3" json:"k8s,omitempty"` K8S bool `protobuf:"varint,2,opt,name=k8s,proto3" json:"k8s,omitempty"`
HostExec bool `protobuf:"varint,3,opt,name=host_exec,json=hostExec,proto3" json:"host_exec,omitempty"` HostExec bool `protobuf:"varint,3,opt,name=host_exec,json=hostExec,proto3" json:"host_exec,omitempty"`
Vnc bool `protobuf:"varint,4,opt,name=vnc,proto3" json:"vnc,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *Capabilities) Reset() { func (x *Capabilities) Reset() {
*x = Capabilities{} *x = Capabilities{}
mi := &file_tunnel_proto_msgTypes[3] mi := &file_tunnel_proto_tunnel_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -291,7 +316,7 @@ func (x *Capabilities) String() string {
func (*Capabilities) ProtoMessage() {} func (*Capabilities) ProtoMessage() {}
func (x *Capabilities) ProtoReflect() protoreflect.Message { func (x *Capabilities) ProtoReflect() protoreflect.Message {
mi := &file_tunnel_proto_msgTypes[3] mi := &file_tunnel_proto_tunnel_proto_msgTypes[3]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -304,7 +329,7 @@ func (x *Capabilities) ProtoReflect() protoreflect.Message {
// Deprecated: Use Capabilities.ProtoReflect.Descriptor instead. // Deprecated: Use Capabilities.ProtoReflect.Descriptor instead.
func (*Capabilities) Descriptor() ([]byte, []int) { func (*Capabilities) Descriptor() ([]byte, []int) {
return file_tunnel_proto_rawDescGZIP(), []int{3} return file_tunnel_proto_tunnel_proto_rawDescGZIP(), []int{3}
} }
func (x *Capabilities) GetDocker() bool { func (x *Capabilities) GetDocker() bool {
@ -328,6 +353,13 @@ func (x *Capabilities) GetHostExec() bool {
return false return false
} }
func (x *Capabilities) GetVnc() bool {
if x != nil {
return x.Vnc
}
return false
}
type SystemInfo struct { type SystemInfo struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Os string `protobuf:"bytes,1,opt,name=os,proto3" json:"os,omitempty"` Os string `protobuf:"bytes,1,opt,name=os,proto3" json:"os,omitempty"`
@ -340,7 +372,7 @@ type SystemInfo struct {
func (x *SystemInfo) Reset() { func (x *SystemInfo) Reset() {
*x = SystemInfo{} *x = SystemInfo{}
mi := &file_tunnel_proto_msgTypes[4] mi := &file_tunnel_proto_tunnel_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -352,7 +384,7 @@ func (x *SystemInfo) String() string {
func (*SystemInfo) ProtoMessage() {} func (*SystemInfo) ProtoMessage() {}
func (x *SystemInfo) ProtoReflect() protoreflect.Message { func (x *SystemInfo) ProtoReflect() protoreflect.Message {
mi := &file_tunnel_proto_msgTypes[4] mi := &file_tunnel_proto_tunnel_proto_msgTypes[4]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -365,7 +397,7 @@ func (x *SystemInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use SystemInfo.ProtoReflect.Descriptor instead. // Deprecated: Use SystemInfo.ProtoReflect.Descriptor instead.
func (*SystemInfo) Descriptor() ([]byte, []int) { func (*SystemInfo) Descriptor() ([]byte, []int) {
return file_tunnel_proto_rawDescGZIP(), []int{4} return file_tunnel_proto_tunnel_proto_rawDescGZIP(), []int{4}
} }
func (x *SystemInfo) GetOs() string { func (x *SystemInfo) GetOs() string {
@ -396,12 +428,12 @@ func (x *SystemInfo) GetShell() string {
return "" return ""
} }
var File_tunnel_proto protoreflect.FileDescriptor var File_tunnel_proto_tunnel_proto protoreflect.FileDescriptor
const file_tunnel_proto_rawDesc = "" + const file_tunnel_proto_tunnel_proto_rawDesc = "" +
"\n" + "\n" +
"\ftunnel.proto\x12\n" + "\x19tunnel/proto/tunnel.proto\x12\n" +
"tai.tunnel\"\xf6\x02\n" + "tai.tunnel\"\xe3\x03\n" +
"\rTunnelControl\x12\x12\n" + "\rTunnelControl\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x17\n" + "\x04type\x18\x01 \x01(\tR\x04type\x12\x17\n" +
"\anode_id\x18\x02 \x01(\tR\x06nodeId\x12\x1d\n" + "\anode_id\x18\x02 \x01(\tR\x06nodeId\x12\x1d\n" +
@ -416,7 +448,10 @@ const file_tunnel_proto_rawDesc = "" +
"channel_id\x18\n" + "channel_id\x18\n" +
" \x01(\tR\tchannelId\x12\x1f\n" + " \x01(\tR\tchannelId\x12\x1f\n" +
"\vtarget_port\x18\v \x01(\x05R\n" + "\vtarget_port\x18\v \x01(\x05R\n" +
"targetPort\x12\x15\n" + "targetPort\x12!\n" +
"\fchannel_type\x18\f \x01(\tR\vchannelType\x12!\n" +
"\fcontainer_id\x18\r \x01(\tR\vcontainerId\x12%\n" +
"\x0econtainer_port\x18\x0e \x01(\x05R\rcontainerPort\x12\x15\n" +
"\x06tai_id\x18\x14 \x01(\tR\x05taiId\"!\n" + "\x06tai_id\x18\x14 \x01(\tR\x05taiId\"!\n" +
"\vForwardData\x12\x12\n" + "\vForwardData\x12\x12\n" +
"\x04data\x18\x01 \x01(\fR\x04data\"k\n" + "\x04data\x18\x01 \x01(\fR\x04data\"k\n" +
@ -425,11 +460,12 @@ const file_tunnel_proto_rawDesc = "" +
"\x04http\x18\x02 \x01(\x05R\x04http\x12\x10\n" + "\x04http\x18\x02 \x01(\x05R\x04http\x12\x10\n" +
"\x03vnc\x18\x03 \x01(\x05R\x03vnc\x12\x16\n" + "\x03vnc\x18\x03 \x01(\x05R\x03vnc\x12\x16\n" +
"\x06docker\x18\x04 \x01(\x05R\x06docker\x12\x10\n" + "\x06docker\x18\x04 \x01(\x05R\x06docker\x12\x10\n" +
"\x03k8s\x18\x05 \x01(\x05R\x03k8s\"U\n" + "\x03k8s\x18\x05 \x01(\x05R\x03k8s\"g\n" +
"\fCapabilities\x12\x16\n" + "\fCapabilities\x12\x16\n" +
"\x06docker\x18\x01 \x01(\bR\x06docker\x12\x10\n" + "\x06docker\x18\x01 \x01(\bR\x06docker\x12\x10\n" +
"\x03k8s\x18\x02 \x01(\bR\x03k8s\x12\x1b\n" + "\x03k8s\x18\x02 \x01(\bR\x03k8s\x12\x1b\n" +
"\thost_exec\x18\x03 \x01(\bR\bhostExec\"b\n" + "\thost_exec\x18\x03 \x01(\bR\bhostExec\x12\x10\n" +
"\x03vnc\x18\x04 \x01(\bR\x03vnc\"b\n" +
"\n" + "\n" +
"SystemInfo\x12\x0e\n" + "SystemInfo\x12\x0e\n" +
"\x02os\x18\x01 \x01(\tR\x02os\x12\x12\n" + "\x02os\x18\x01 \x01(\tR\x02os\x12\x12\n" +
@ -438,29 +474,29 @@ const file_tunnel_proto_rawDesc = "" +
"\x05shell\x18\x04 \x01(\tR\x05shell2\x92\x01\n" + "\x05shell\x18\x04 \x01(\tR\x05shell2\x92\x01\n" +
"\tTaiTunnel\x12D\n" + "\tTaiTunnel\x12D\n" +
"\bRegister\x12\x19.tai.tunnel.TunnelControl\x1a\x19.tai.tunnel.TunnelControl(\x010\x01\x12?\n" + "\bRegister\x12\x19.tai.tunnel.TunnelControl\x1a\x19.tai.tunnel.TunnelControl(\x010\x01\x12?\n" +
"\aForward\x12\x17.tai.tunnel.ForwardData\x1a\x17.tai.tunnel.ForwardData(\x010\x01B(Z&github.com/yaoapp/yao/tai/tunnel/taipbb\x06proto3" "\aForward\x12\x17.tai.tunnel.ForwardData\x1a\x17.tai.tunnel.ForwardData(\x010\x01B$Z\"github.com/yaoapp/tai/tunnel/taipbb\x06proto3"
var ( var (
file_tunnel_proto_rawDescOnce sync.Once file_tunnel_proto_tunnel_proto_rawDescOnce sync.Once
file_tunnel_proto_rawDescData []byte file_tunnel_proto_tunnel_proto_rawDescData []byte
) )
func file_tunnel_proto_rawDescGZIP() []byte { func file_tunnel_proto_tunnel_proto_rawDescGZIP() []byte {
file_tunnel_proto_rawDescOnce.Do(func() { file_tunnel_proto_tunnel_proto_rawDescOnce.Do(func() {
file_tunnel_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_tunnel_proto_rawDesc), len(file_tunnel_proto_rawDesc))) file_tunnel_proto_tunnel_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_tunnel_proto_tunnel_proto_rawDesc), len(file_tunnel_proto_tunnel_proto_rawDesc)))
}) })
return file_tunnel_proto_rawDescData return file_tunnel_proto_tunnel_proto_rawDescData
} }
var file_tunnel_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_tunnel_proto_tunnel_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_tunnel_proto_goTypes = []any{ var file_tunnel_proto_tunnel_proto_goTypes = []any{
(*TunnelControl)(nil), // 0: tai.tunnel.TunnelControl (*TunnelControl)(nil), // 0: tai.tunnel.TunnelControl
(*ForwardData)(nil), // 1: tai.tunnel.ForwardData (*ForwardData)(nil), // 1: tai.tunnel.ForwardData
(*Ports)(nil), // 2: tai.tunnel.Ports (*Ports)(nil), // 2: tai.tunnel.Ports
(*Capabilities)(nil), // 3: tai.tunnel.Capabilities (*Capabilities)(nil), // 3: tai.tunnel.Capabilities
(*SystemInfo)(nil), // 4: tai.tunnel.SystemInfo (*SystemInfo)(nil), // 4: tai.tunnel.SystemInfo
} }
var file_tunnel_proto_depIdxs = []int32{ var file_tunnel_proto_tunnel_proto_depIdxs = []int32{
2, // 0: tai.tunnel.TunnelControl.ports:type_name -> tai.tunnel.Ports 2, // 0: tai.tunnel.TunnelControl.ports:type_name -> tai.tunnel.Ports
3, // 1: tai.tunnel.TunnelControl.caps:type_name -> tai.tunnel.Capabilities 3, // 1: tai.tunnel.TunnelControl.caps:type_name -> tai.tunnel.Capabilities
4, // 2: tai.tunnel.TunnelControl.system:type_name -> tai.tunnel.SystemInfo 4, // 2: tai.tunnel.TunnelControl.system:type_name -> tai.tunnel.SystemInfo
@ -475,26 +511,26 @@ var file_tunnel_proto_depIdxs = []int32{
0, // [0:3] is the sub-list for field type_name 0, // [0:3] is the sub-list for field type_name
} }
func init() { file_tunnel_proto_init() } func init() { file_tunnel_proto_tunnel_proto_init() }
func file_tunnel_proto_init() { func file_tunnel_proto_tunnel_proto_init() {
if File_tunnel_proto != nil { if File_tunnel_proto_tunnel_proto != nil {
return return
} }
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_tunnel_proto_rawDesc), len(file_tunnel_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_tunnel_proto_tunnel_proto_rawDesc), len(file_tunnel_proto_tunnel_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 5, NumMessages: 5,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },
GoTypes: file_tunnel_proto_goTypes, GoTypes: file_tunnel_proto_tunnel_proto_goTypes,
DependencyIndexes: file_tunnel_proto_depIdxs, DependencyIndexes: file_tunnel_proto_tunnel_proto_depIdxs,
MessageInfos: file_tunnel_proto_msgTypes, MessageInfos: file_tunnel_proto_tunnel_proto_msgTypes,
}.Build() }.Build()
File_tunnel_proto = out.File File_tunnel_proto_tunnel_proto = out.File
file_tunnel_proto_goTypes = nil file_tunnel_proto_tunnel_proto_goTypes = nil
file_tunnel_proto_depIdxs = nil file_tunnel_proto_tunnel_proto_depIdxs = nil
} }

View file

@ -2,7 +2,7 @@
// versions: // versions:
// - protoc-gen-go-grpc v1.6.1 // - protoc-gen-go-grpc v1.6.1
// - protoc v4.25.0 // - protoc v4.25.0
// source: tunnel.proto // source: tunnel/proto/tunnel.proto
package taipb package taipb
@ -147,5 +147,5 @@ var TaiTunnel_ServiceDesc = grpc.ServiceDesc{
ClientStreams: true, ClientStreams: true,
}, },
}, },
Metadata: "tunnel.proto", Metadata: "tunnel/proto/tunnel.proto",
} }

View file

@ -24,6 +24,7 @@ type Capabilities struct {
Docker bool `json:"docker"` Docker bool `json:"docker"`
K8s bool `json:"k8s"` K8s bool `json:"k8s"`
HostExec bool `json:"host_exec"` HostExec bool `json:"host_exec"`
VNC bool `json:"vnc"`
} }
// SystemInfo describes the host machine running Tai. // SystemInfo describes the host machine running Tai.