- 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>
144 lines
3.7 KiB
Go
144 lines
3.7 KiB
Go
// Package tsnetbridge 是 tsnet 的最小 gomobile 桥接探针(M1 可行性验证)。
|
||
// 目标:证明 tsnet 能经 gomobile bind 编译到 iOS(retire R5:Go+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 启动并等待 Running(timeoutMs 到则返回 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.Conn(netstack 虚拟连接,无真实 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()
|
||
}
|