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

@@ -0,0 +1,50 @@
import Foundation
/// 解析 `mosh-server new` 打印的握手行 `MOSH CONNECT <port> <key>`。
///
/// 引导流程:经 SSH 跑 `mosh-server new …` → 服务端 detach 并在 stdout 打印这一行 →
/// 客户端据此用 UDP + key 连到 host:port。因 SSH 流里混有提示符/回显,采用流式扫描:
/// 逐段 `feed` 追加缓冲并尝试匹配,命中即返回(幂等,多次命中以首次为准)。
public struct MoshConnectInfo: Equatable, Sendable {
public let port: Int
public let key: String
public init(port: Int, key: String) { self.port = port; self.key = key }
}
public final class MoshConnectScanner {
private var buffer = ""
private var found: MoshConnectInfo?
public init() {}
/// 追加一段输出并尝试匹配;返回本次(或此前)解析到的握手信息(若有)。
@discardableResult
public func feed(_ text: String) -> MoshConnectInfo? {
if let found { return found }
buffer += text
// 只保留尾部,避免无限增长(握手行很短,8KB 足够跨 feed 边界)。
if buffer.count > 8192 { buffer = String(buffer.suffix(8192)) }
if let info = Self.parse(buffer) { found = info; return info }
return nil
}
public var result: MoshConnectInfo? { found }
/// 从任意文本中提取第一处 `MOSH CONNECT <port> <key>`。key 为 base64(AES-128 会话密钥)。
public static func parse(_ text: String) -> MoshConnectInfo? {
guard let range = text.range(of: "MOSH CONNECT ") else { return nil }
let rest = text[range.upperBound...]
// 取该 token 起至行尾,按空白切分:port key。
// 注意:Swift 把 "\r\n" 视为单个 Character(扩展字形簇),不能用 $0 != "\r" 判行尾,
// 必须用 isNewline(对 \n / \r / \r\n 均为真)。
let line = rest.split(whereSeparator: { $0.isNewline }).first ?? ""
let parts = line.split(whereSeparator: { $0 == " " || $0 == "\t" })
guard parts.count >= 2, let port = Int(parts[0]) else { return nil }
let key = String(parts[1])
// key 须为纯 base64 字符集且非空,避免把提示符碎片误当 key。
guard !key.isEmpty, key.allSatisfy({ $0.isLetter || $0.isNumber || $0 == "+" || $0 == "/" || $0 == "=" }) else {
return nil
}
return MoshConnectInfo(port: port, key: key)
}
}

View File

@@ -0,0 +1,37 @@
import XCTest
@testable import TXCore
final class MoshConnectTests: XCTestCase {
func testParseBasic() {
let info = MoshConnectScanner.parse("MOSH CONNECT 60001 gPFm3s8bkfMGrun5QI0Xdw")
XCTAssertEqual(info, MoshConnectInfo(port: 60001, key: "gPFm3s8bkfMGrun5QI0Xdw"))
}
func testParseAmidNoise() {
let out = """
user@host:~$ mosh-server new -s -c 256\r
\r
MOSH CONNECT 60022 AbCd1234+/xyzKEY0000==\r
\r
mosh-server (mosh 1.4.0)\r
"""
let info = MoshConnectScanner.parse(out)
XCTAssertEqual(info?.port, 60022)
XCTAssertEqual(info?.key, "AbCd1234+/xyzKEY0000==")
}
func testNoMatch() {
XCTAssertNil(MoshConnectScanner.parse("bash: mosh-server: command not found"))
XCTAssertNil(MoshConnectScanner.parse("MOSH CONNECT notaport keyhere"))
}
func testStreamingAcrossFeeds() {
let s = MoshConnectScanner()
XCTAssertNil(s.feed("prompt$ mosh-server new\r\nMOSH CON"))
XCTAssertNil(s.result)
let info = s.feed("NECT 60007 KeyAcrossBoundary123\r\n")
XCTAssertEqual(info, MoshConnectInfo(port: 60007, key: "KeyAcrossBoundary123"))
// 幂等:后续 feed 不改变结果。
XCTAssertEqual(s.feed("MOSH CONNECT 61000 other"), MoshConnectInfo(port: 60007, key: "KeyAcrossBoundary123"))
}
}

View File

@@ -18,14 +18,26 @@ let package = Package(
.binaryTarget(name: "CSSHCore", path: "../../artifacts/CSSHCore.xcframework"),
// 头文件 + modulemap 载体(供 `import CSSH`);库符号来自 CSSHCore。
.target(name: "CSSH", publicHeadersPath: "include"),
// mosh(C++) + protobuf-lite 静态库切片(vendor/build/build-mosh.sh,library-only)。
.binaryTarget(name: "MoshCore", path: "../../artifacts/MoshCore.xcframework"),
// 头文件 + modulemap 载体(供 `import CMosh` 调 mosh_main);库符号来自 MoshCore。
.target(name: "CMosh", publicHeadersPath: "include"),
.target(
name: "TXTransport",
dependencies: [
"CSSH",
"CSSHCore",
"CMosh",
"MoshCore",
.product(name: "TXCore", package: "TXCore"),
],
swiftSettings: [.swiftLanguageMode(.v6)]
swiftSettings: [.swiftLanguageMode(.v6)],
// mosh 是 C++(需 libc++)且 compressor.cc 用 zlib(需 libz);
// CommonCrypto 属 libSystem 伞库,Apple 平台自动解析无需显式链接。
linkerSettings: [
.linkedLibrary("c++"),
.linkedLibrary("z"),
]
),
]
)

View File

@@ -0,0 +1,4 @@
module CMosh {
header "moshiosbridge.h"
export *
}

View File

@@ -0,0 +1,17 @@
/* CMosh — Swift 可 import 的 mosh 客户端入口头(库符号来自 MoshCore.xcframework)。
* 对应 vendor/mosh/src/frontend/moshiosbridge.h,保持签名一致。
* mosh_main 是阻塞式主循环:读 f_in(用户输入字节)、写 f_out(终端输出字节),
* 经 UDP 连 ip:port(mosh SSP,AES-128-OCB),window_size 供 SIGWINCH resize。 */
#ifndef CMOSH_MOSHIOSBRIDGE_H
#define CMOSH_MOSHIOSBRIDGE_H
#include <stdio.h>
#include <sys/ioctl.h>
#if __cplusplus
extern "C"
#endif
int mosh_main(FILE *f_in, FILE *f_out, struct winsize *window_size,
const char *ip, const char *port, const char *key, const char *predict_mode);
#endif /* CMOSH_MOSHIOSBRIDGE_H */

View File

@@ -0,0 +1,3 @@
// CMosh 是 mosh_main 头文件 + modulemap 的载体 target(供 `import CMosh`);
// 实际库符号来自 MoshCore.xcframework(静态库)。此处仅需一个非空翻译单元。
typedef int cmosh_shim_placeholder;

View File

@@ -0,0 +1,162 @@
import Foundation
import CMosh
/// MoshSession — 驱动 vendored mosh 客户端(MoshCore.xcframework 的 `mosh_main`)。
///
/// 桥接模型:`mosh_main` 是阻塞式主循环,用 `FILE*` 读用户输入、写终端输出。
/// - 输入:Swift `send(_:)` 写入 inPipe[1] → mosh 从 `fileno(f_in)=inPipe[0]` 读。
/// - 输出:mosh 写 `f_out=outPipe[1]` → 后台读线程从 outPipe[0] 读 → `onBytes`。
/// - resize:更新共享 `winsize` 结构后 `pthread_kill(SIGWINCH)`,mosh 的 select 循环据此重绘。
///
/// 连接目标由调用方给定 ip:port —— mosh-over-tsnet 时传 `127.0.0.1:<relay 本地端口>`
/// (见 TsnetBridge 的 MoshRelay),mosh 感知不到 tailnet 的存在。
public final class MoshSession: @unchecked Sendable {
/// mosh 输出的终端字节(送入 libghostty)。
public var onBytes: (@Sendable ([UInt8]) -> Void)?
/// mosh_main 返回(会话结束)时回调,携带返回码(0=成功退出)。
public var onClosed: (@Sendable (Int32) -> Void)?
private let ip: String
private let port: String
private let key: String
private let predictMode: String
private let winPtr = UnsafeMutablePointer<winsize>.allocate(capacity: 1)
private let lock = NSLock()
private var inWriteFD: Int32 = -1 // Swift 写用户输入
private var outReadFD: Int32 = -1 // Swift 读终端输出
private var moshThread: pthread_t?
private var started = false
private var closed = false
/// - Parameters:
/// - ip/port: mosh-server 的可达地址(tsnet 场景为 loopback relay 地址)。
/// - key: `mosh-server new` 打印的 MOSH_KEY(base64 会话密钥)。
/// - predictMode: always | never | adaptive | experimental(默认 adaptive)。
public init(ip: String, port: Int, key: String, cols: Int, rows: Int, predictMode: String = "adaptive") {
self.ip = ip
self.port = String(port)
self.key = key
self.predictMode = predictMode
winPtr.pointee = winsize(ws_row: UInt16(rows), ws_col: UInt16(cols), ws_xpixel: 0, ws_ypixel: 0)
}
deinit {
winPtr.deallocate()
}
/// 建立 pipe、启动读线程与 mosh 主循环线程。可多次调用无副作用(仅首次生效)。
public func start() {
lock.lock()
if started { lock.unlock(); return }
started = true
lock.unlock()
// 写关闭的 pipe 会触发 SIGPIPE(默认杀进程);改为返回 EPIPE。
signal(SIGPIPE, SIG_IGN)
// mosh 要求 UTF-8 native locale,否则 is_utf8_locale() 失败 → exit(1)。
setenv("LANG", "en_US.UTF-8", 1)
setenv("LC_ALL", "en_US.UTF-8", 1)
var inPipe: [Int32] = [-1, -1]
var outPipe: [Int32] = [-1, -1]
_ = inPipe.withUnsafeMutableBufferPointer { pipe($0.baseAddress) }
_ = outPipe.withUnsafeMutableBufferPointer { pipe($0.baseAddress) }
lock.lock()
inWriteFD = inPipe[1]
outReadFD = outPipe[0]
lock.unlock()
startReader(fd: outPipe[0])
startMosh(inReadFD: inPipe[0], outWriteFD: outPipe[1])
}
/// 写入用户输入字节(键盘)。
public func send(_ bytes: [UInt8]) {
lock.lock(); let fd = inWriteFD; lock.unlock()
guard fd >= 0, !bytes.isEmpty else { return }
var off = 0
bytes.withUnsafeBytes { buf in
while off < buf.count {
let n = write(fd, buf.baseAddress!.advanced(by: off), buf.count - off)
if n <= 0 { break }
off += n
}
}
}
/// 更新终端尺寸并通知 mosh(SIGWINCH → process_resize)。
public func resize(cols: Int, rows: Int) {
winPtr.pointee.ws_col = UInt16(cols)
winPtr.pointee.ws_row = UInt16(rows)
lock.lock(); let tid = moshThread; lock.unlock()
if let tid { pthread_kill(tid, SIGWINCH) }
}
/// 关闭会话:关掉 pipe(mosh 侧 select 遇 EOF/EPIPE),读线程随即结束。
public func close() {
lock.lock()
if closed { lock.unlock(); return }
closed = true
let w = inWriteFD, r = outReadFD
inWriteFD = -1; outReadFD = -1
lock.unlock()
if w >= 0 { Foundation.close(w) }
if r >= 0 { Foundation.close(r) }
}
// MARK: - 私有
private func setMoshThread(_ tid: pthread_t) { lock.lock(); moshThread = tid; lock.unlock() }
private func handleExit(_ rc: Int32) {
lock.lock(); moshThread = nil; lock.unlock()
onClosed?(rc)
}
/// mosh 主循环线程:fdopen 两端 FILE*,调用阻塞的 mosh_main。用 Foundation.Thread
/// (而非 pthread_create + Unmanaged 上下文)以规避 Swift6 SendNonSendable pass 的编译器崩溃;
/// 线程内 pthread_self() 取得 tid 供 SIGWINCH。
private func startMosh(inReadFD: Int32, outWriteFD: Int32) {
let ip = self.ip, port = self.port, key = self.key, predict = self.predictMode
let winPtr = self.winPtr
let t = Thread { [weak self] in
self?.setMoshThread(pthread_self())
guard let fin = fdopen(inReadFD, "r"), let fout = fdopen(outWriteFD, "w") else {
self?.handleExit(-1); return
}
setvbuf(fout, nil, _IONBF, 0) // mosh 用 fwrite 写 out_fd,须无缓冲才能实时流出
let rc = ip.withCString { ipC in
port.withCString { portC in
key.withCString { keyC in
predict.withCString { prC in
mosh_main(fin, fout, winPtr, ipC, portC, keyC, prC)
}
}
}
}
fflush(fout)
self?.handleExit(rc)
}
t.name = "mosh-main"
t.stackSize = 2 * 1024 * 1024
t.start()
}
/// 后台读线程:把 mosh 输出字节泵给 onBytes。
private func startReader(fd: Int32) {
let t = Thread { [weak self] in
var buf = [UInt8](repeating: 0, count: 16384)
while true {
let n = buf.withUnsafeMutableBytes { read(fd, $0.baseAddress, $0.count) }
if n <= 0 { break }
let chunk = Array(buf[0..<n])
self?.onBytes?(chunk)
}
}
t.name = "mosh-reader"
t.stackSize = 512 * 1024
t.start()
}
}