- mosh(blinksh/ios C++)+protobuf-lite 交叉编译为 MoshCore.xcframework (CommonCrypto 后端、含 arm64 模拟器 slice;绕 autotools 用 CMake 直编 + 手写 config.h) - tsnet 桥新增 UDP relay(StartMoshRelay:loopback↔tsnet.Dial 逐包搬运) - MoshSession(pipe↔FILE*/pthread/SIGWINCH)+ MoshConnectScanner(TXCore,+4 单测=39) - SSHTerminalModel 编排:SSH 引导 mosh-server→解析 MOSH CONNECT→relay→mosh 接管 - LAN 直连 + mosh-over-tsnet 均端到端验证通过,R1(tsnet UDP 数据报语义) 证伪 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
222 lines
6.1 KiB
Go
222 lines
6.1 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"
|
||
"sync/atomic"
|
||
"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
|
||
}
|
||
|
||
// MoshRelay 是一条 mosh-over-tsnet 的 UDP 中继:
|
||
// mosh-client 在本地把 UDP 包发到 127.0.0.1:<LocalPort>,本中继逐包经 tsnet 转发到
|
||
// tailnet 内 host:moshPort,回包再转回"最近一次上行的 client 源地址"(吸收 mosh 的 roaming/端口跳变)。
|
||
// 与 DialTCPFD 的差异:mosh 内部自建 UDP socket 并 connect(),无法注入外部 fd,故用 loopback relay。
|
||
type MoshRelay struct {
|
||
local *net.UDPConn // 真实内核 loopback UDP socket(mosh-client 的对端)
|
||
tsConn net.Conn // tsnet netstack UDP 虚拟连接(到 mosh-server)
|
||
lastCli atomic.Pointer[net.UDPAddr]
|
||
}
|
||
|
||
// StartMoshRelay 建立中继并启动双向泵,返回句柄。Swift 用 LocalPort() 作为 mosh_main 的 port
|
||
// 参数、ip 传 "127.0.0.1"。失败返回 nil + error。timeoutMs 限制 tsnet UDP Dial 的建连等待。
|
||
func (n *Node) StartMoshRelay(host string, moshPort int, timeoutMs int) (*MoshRelay, error) {
|
||
ctx, cancel := context.WithTimeout(context.Background(),
|
||
time.Duration(timeoutMs)*time.Millisecond)
|
||
defer cancel()
|
||
|
||
tsConn, err := n.srv.Dial(ctx, "udp", net.JoinHostPort(host, strconv.Itoa(moshPort)))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
local, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||
if err != nil {
|
||
tsConn.Close()
|
||
return nil, err
|
||
}
|
||
|
||
r := &MoshRelay{local: local, tsConn: tsConn}
|
||
|
||
// 上行:client(loopback) → tsnet(server)。记录最近 client 源地址供下行回送。
|
||
go func() {
|
||
buf := make([]byte, 65536)
|
||
for {
|
||
nr, cliAddr, err := local.ReadFromUDP(buf)
|
||
if nr > 0 {
|
||
r.lastCli.Store(cliAddr)
|
||
if _, werr := tsConn.Write(buf[:nr]); werr != nil {
|
||
return
|
||
}
|
||
}
|
||
if err != nil {
|
||
return
|
||
}
|
||
}
|
||
}()
|
||
// 下行:tsnet(server) → client(loopback 最近源地址)。逐报文保边界。
|
||
go func() {
|
||
buf := make([]byte, 65536)
|
||
for {
|
||
nr, err := tsConn.Read(buf)
|
||
if nr > 0 {
|
||
if cli := r.lastCli.Load(); cli != nil {
|
||
if _, werr := local.WriteToUDP(buf[:nr], cli); werr != nil {
|
||
return
|
||
}
|
||
}
|
||
}
|
||
if err != nil {
|
||
return
|
||
}
|
||
}
|
||
}()
|
||
|
||
return r, nil
|
||
}
|
||
|
||
// LocalPort 返回 loopback relay 监听的端口(mosh_main 的 port 参数)。
|
||
func (r *MoshRelay) LocalPort() int {
|
||
return r.local.LocalAddr().(*net.UDPAddr).Port
|
||
}
|
||
|
||
// Close 关闭中继(两条泵 goroutine 随即因 conn 关闭而退出)。
|
||
func (r *MoshRelay) Close() error {
|
||
r.local.Close()
|
||
return r.tsConn.Close()
|
||
}
|
||
|
||
// 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()
|
||
}
|