Files
terminalX/vendor/tsnet-bridge/tsnetbridge/bridge.go
kid aa92d0e676 初始提交:terminalX 可运行态(M0/M1/M1.5 已真机验证)
- M0: libghostty SSH 终端(渲染/输入/连接)+ 白屏修复(OutputGate) + 会话状态机/自动重连
- M1: tsnet 用户态组网 + SSH-over-tsnet(fd 桥),shell 级真机验证;R5(Go+gvisor+C+++Swift 同进程) retire
- M1.5: tmux -CC 原生 tab(MVP)
- 结构: packages/(TXCore·TXTransport), apps/TerminalX, vendor/(libghostty-spm/libssh2/mbedtls/tsnet-bridge), artifacts/
- 文档: CLAUDE.md + docs/HANDOFF.md(新会话入口)
- 环境: 认证代理→依赖 vendor 本地化;Go 在 ~/.local/go;仅模拟器/未签名
- 待续: M2 mosh, tmux 多 pane, M4 安全(host key/SE)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 10:20:46 +08:00

144 lines
3.7 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 tsnetbridge 是 tsnet 的最小 gomobile 桥接探针M1 可行性验证)。
// 目标:证明 tsnet 能经 gomobile bind 编译到 iOSretire R5Go+C+++Swift 三运行时共存)。
// 真实的多节点/DialerRouter/fd 桥实现见设计文档,后续填充。
package tsnetbridge
import (
"context"
"encoding/json"
"io"
"net"
"os"
"strconv"
"time"
"golang.org/x/sys/unix"
"tailscale.com/tsnet"
)
// Node 包装一个嵌入式 tsnet 节点。gomobile 导出为引用型对象。
type Node struct {
srv *tsnet.Server
}
// NewNode 创建(不启动)一个 tsnet 节点。stateDir 唯一、hostname 展示在管理台。
func NewNode(stateDir, hostname string) *Node {
return &Node{srv: &tsnet.Server{
Dir: stateDir,
Hostname: hostname,
Ephemeral: false,
}}
}
// UpWithAuthKey 用 auth key 启动并等待 RunningtimeoutMs 到则返回 error
// gomobile 友好签名基本类型入参、error 出参。
func (n *Node) UpWithAuthKey(authKey string, timeoutMs int) error {
n.srv.AuthKey = authKey
ctx, cancel := context.WithTimeout(context.Background(),
time.Duration(timeoutMs)*time.Millisecond)
defer cancel()
_, err := n.srv.Up(ctx)
return err
}
// SelfIP 返回本节点的 Tailscale IPv4经 LocalClient().Status() 取;未就绪返回空串)。
func (n *Node) SelfIP() string {
lc, err := n.srv.LocalClient()
if err != nil {
return ""
}
st, err := lc.Status(context.Background())
if err != nil || st.Self == nil {
return ""
}
for _, ip := range st.Self.TailscaleIPs {
if ip.Is4() {
return ip.String()
}
}
return ""
}
// DialTCPFD 经 tsnet 连接 tailnet 内 host:port返回一个已连接的 socket fd 供 libssh2 使用。
// 实现tsnet.Dial 得到 net.Connnetstack 虚拟连接,无真实 fd→ socketpair(AF_UNIX) →
// 两条 goroutine 双向 io.Copy 泵 → 把 socketpair 的另一端 fd 返回给 Swift。
// Swift 侧 close(fd) 会触发泵结束并关闭 tsConn。返回 -1 + error 表示失败。
func (n *Node) DialTCPFD(hostOrIP string, port int, timeoutMs int) (int64, error) {
ctx, cancel := context.WithTimeout(context.Background(),
time.Duration(timeoutMs)*time.Millisecond)
defer cancel()
tsConn, err := n.srv.Dial(ctx, "tcp", net.JoinHostPort(hostOrIP, strconv.Itoa(port)))
if err != nil {
return -1, err
}
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
if err != nil {
tsConn.Close()
return -1, err
}
goFile := os.NewFile(uintptr(fds[0]), "ts-pump")
goConn, err := net.FileConn(goFile)
goFile.Close()
if err != nil {
tsConn.Close()
unix.Close(fds[0])
unix.Close(fds[1])
return -1, err
}
// 双向泵:任一方向结束即关闭两端。
go func() {
io.Copy(goConn, tsConn)
goConn.Close()
tsConn.Close()
}()
go func() {
io.Copy(tsConn, goConn)
tsConn.Close()
goConn.Close()
}()
return int64(fds[1]), nil
}
// PeersJSON 返回 tailnet 内其它节点列表name/ip/online/os的 JSON用于选择连接目标。
func (n *Node) PeersJSON() string {
lc, err := n.srv.LocalClient()
if err != nil {
return "[]"
}
st, err := lc.Status(context.Background())
if err != nil {
return "[]"
}
type peer struct {
Name string `json:"name"`
IP string `json:"ip"`
Online bool `json:"online"`
OS string `json:"os"`
}
out := []peer{}
for _, p := range st.Peer {
ip := ""
for _, a := range p.TailscaleIPs {
if a.Is4() {
ip = a.String()
break
}
}
out = append(out, peer{Name: p.DNSName, IP: ip, Online: p.Online, OS: p.OS})
}
b, err := json.Marshal(out)
if err != nil {
return "[]"
}
return string(b)
}
// Close 释放节点。
func (n *Node) Close() error {
return n.srv.Close()
}