feat: 完成 M2 mosh(LAN + tsnet 端到端验证)

- 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>
This commit is contained in:
kid
2026-07-24 16:14:40 +08:00
parent aa92d0e676
commit 32520f09e0
1232 changed files with 728138 additions and 37 deletions

View File

@@ -10,6 +10,7 @@ import (
"net"
"os"
"strconv"
"sync/atomic"
"time"
"golang.org/x/sys/unix"
@@ -103,6 +104,83 @@ func (n *Node) DialTCPFD(hostOrIP string, port int, timeoutMs int) (int64, error
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 socketmosh-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()