初始提交: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>
This commit is contained in:
143
vendor/tsnet-bridge/tsnetbridge/bridge.go
vendored
Normal file
143
vendor/tsnet-bridge/tsnetbridge/bridge.go
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
// 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()
|
||||
}
|
||||
Reference in New Issue
Block a user