From 521f4e55a6299229034888d108f9a82c002ca46b Mon Sep 17 00:00:00 2001 From: qiin2333 <414382190@qq.com> Date: Fri, 11 Sep 2026 14:35:23 +0800 Subject: [PATCH 1/8] chore(deps): bump moonlight-common-c to 31a2a45 Pulls in remote text context updates (#28), opt-in dynamic HDR wire constants (#26) and received-video-bytes stats (#27). The new RemoteTextContextStream.c joins the CMake source list (missing it breaks the final link with an undefined decodeRemoteTextContextPacket). Co-Authored-By: Claude Fable 5 --- nativelib/src/main/cpp/CMakeLists.txt | 1 + nativelib/src/main/cpp/moonlight-common-c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/nativelib/src/main/cpp/CMakeLists.txt b/nativelib/src/main/cpp/CMakeLists.txt index 9342698..cb351e0 100644 --- a/nativelib/src/main/cpp/CMakeLists.txt +++ b/nativelib/src/main/cpp/CMakeLists.txt @@ -131,6 +131,7 @@ set(MOONLIGHT_COMMON_SOURCES ${MOONLIGHT_COMMON_PATH}/src/SimpleStun.c ${MOONLIGHT_COMMON_PATH}/src/ConnectionTester.c ${MOONLIGHT_COMMON_PATH}/src/MicrophoneStream.c + ${MOONLIGHT_COMMON_PATH}/src/RemoteTextContextStream.c ${MOONLIGHT_COMMON_PATH}/src/FakeCallbacks.c ${MOONLIGHT_COMMON_PATH}/nanors/rs.c ${MOONLIGHT_COMMON_PATH}/nanors/deps/obl/oblas_common.c diff --git a/nativelib/src/main/cpp/moonlight-common-c b/nativelib/src/main/cpp/moonlight-common-c index 23433e6..31a2a45 160000 --- a/nativelib/src/main/cpp/moonlight-common-c +++ b/nativelib/src/main/cpp/moonlight-common-c @@ -1 +1 @@ -Subproject commit 23433e6120a15c9b70e952af3757eaa8e9f0ae4d +Subproject commit 31a2a4589ea926988a08ca508bb317fbfbe2a177 From 25b5461ecb517423f94419c957635c804b06eedb Mon Sep 17 00:00:00 2001 From: qiin2333 <414382190@qq.com> Date: Fri, 11 Sep 2026 14:35:30 +0800 Subject: [PATCH 2/8] chore(release): changelog and version catch-up for 1.0.0.807-811 Backfills release notes for #121/#123/#125-128 that shipped without CHANGELOG entries in this checkout. Co-Authored-By: Claude Fable 5 --- AppScope/app.json5 | 4 ++-- entry/src/main/resources/rawfile/CHANGELOG.md | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/AppScope/app.json5 b/AppScope/app.json5 index ad4c7b4..cb8405b 100644 --- a/AppScope/app.json5 +++ b/AppScope/app.json5 @@ -2,8 +2,8 @@ "app": { "bundleName": "com.alkaidlab.sdream", "vendor": "Moonlight", - "versionCode": 1000807, - "versionName": "1.0.0.807", + "versionCode": 1000811, + "versionName": "1.0.0.811", "icon": "$media:layered_icon", "label": "$string:app_name", "bundleType": "app", diff --git a/entry/src/main/resources/rawfile/CHANGELOG.md b/entry/src/main/resources/rawfile/CHANGELOG.md index 2b53933..c475553 100644 --- a/entry/src/main/resources/rawfile/CHANGELOG.md +++ b/entry/src/main/resources/rawfile/CHANGELOG.md @@ -25,6 +25,26 @@ - 最新版本放在最前面 --> +## [1.0.0.811] - 2026-09-04 +输入法三指手势收起稳定性修复 + +### 修复 +- 修复三指手势收起输入法后因触摸事件循环导致输入法反复弹出的问题(#128)。 + +## [1.0.0.810] - 2026-08-31 +网络调度、连接信息与 DualSense 输入体验优化 + +### 新增 +- 支持将 DualSense 电量与原生触控板输入转发至串流主机(#121)。 +- 为串流中的不同网络数据流提供更精细的调度优化,提升复杂网络环境下的稳定性(#127)。 + +### 优化 +- 主机卡片与详情页会正确显示自定义 HTTP/HTTPS 端口,IPv6 地址展示更加清晰(#125)。 +- 性能浮层改为显示真实网络丢包,避免将其他类型的丢帧计入网络质量统计(#126)。 + +### 修复 +- 修复体感数据上报间隔单位不准确的问题,提升动作传感器输入稳定性(#123)。 + +## [1.0.0.812] - 2026-09-11 +OTG 手柄 USB 直通主机(USB/IP 反向隧道,实验性) + +### 新增 +- 通过 OTG 连接的手柄可经 USB/IP 反向隧道直通 Sunshine 主机,作为原生 USB 设备工作:陀螺仪、触控板等完整特性由主机直接驱动,输入不经网络手柄协议转译。 +- 主机侧通过配对证书 + 一次性令牌建立 TLS 反向隧道(Sunshine `usbip-forwarding` 能力),与 moonlight-qt / moonlight-android 的 USB 转发同源契约。 +- 本地 USB/IP 服务仅接受隧道专用回环端口连接,其他本机进程无法访问被导出的设备。 +- 设置中新增「USB 直通主机(实验)」开关;被转发的手柄自动从本地 USB 驱动中排除,转发失败时回退为本地驱动处理。 +- 串流菜单新增「USB 直通主机」状态项:实时显示转发状态(连接中/已直通/失败原因),可在串流中直接开启、重试或停止转发。 +- 当前每次串流转发一个手柄;需 Sunshine 开启 USB 转发并安装 usbip-win2 驱动。 + ## [1.0.0.811] - 2026-09-04 输入法三指手势收起稳定性修复 diff --git a/nativelib/src/main/cpp/CMakeLists.txt b/nativelib/src/main/cpp/CMakeLists.txt index cb351e0..f2877cd 100644 --- a/nativelib/src/main/cpp/CMakeLists.txt +++ b/nativelib/src/main/cpp/CMakeLists.txt @@ -98,6 +98,9 @@ set(SOURCE_FILES mouse_interceptor.cpp usb_helper.cpp usb_ddk_poller.cpp + usbip_server.cpp + usbip_tunnel.cpp + usbip_napi.cpp native_render.cpp presentation_scheduler.cpp two_step_presentation_scheduler.cpp diff --git a/nativelib/src/main/cpp/napi_init.cpp b/nativelib/src/main/cpp/napi_init.cpp index 7e93801..cbed13e 100644 --- a/nativelib/src/main/cpp/napi_init.cpp +++ b/nativelib/src/main/cpp/napi_init.cpp @@ -26,6 +26,7 @@ #include "mouse_interceptor.h" #include "usb_helper.h" #include "usb_ddk_poller.h" +#include "usbip_napi.h" // SDL3 库尚未移植到 HarmonyOS,暂时禁用 // #include "sdl3/sdl3_gamepad_napi.h" @@ -175,6 +176,9 @@ static napi_value Init(napi_env env, napi_value exports) { // 初始化 USB DDK Poller NAPI (DDK 高速轮询) UsbDdkPoller_Init(env, exports); + + // 初始化 USB/IP 反向隧道 NAPI (OTG 设备转发给 Sunshine) + usbip::UsbIpNapi_Init(env, exports); // SDL3 库尚未移植到 HarmonyOS,SDL3 NAPI 暂时禁用 // 当前使用内置的 SDL GameControllerDB 映射数据替代 diff --git a/nativelib/src/main/cpp/usbip_napi.cpp b/nativelib/src/main/cpp/usbip_napi.cpp new file mode 100644 index 0000000..e239289 --- /dev/null +++ b/nativelib/src/main/cpp/usbip_napi.cpp @@ -0,0 +1,379 @@ +/* + * Moonlight for HarmonyOS + * Copyright (C) 2025 Moonlight/AlkaidLab + * + * usbip_napi.cpp - NAPI glue for the USB/IP server + reverse tunnel. + */ + +#include "usbip_napi.h" + +#include +#include +#include +#include + +#include +#include + +#include "usbip_server.h" +#include "usbip_tunnel.h" + +#define LOG_TAG "UsbIpNapi" +#define LOGI(...) OH_LOG_INFO(LOG_APP, __VA_ARGS__) + +namespace usbip { + +namespace { + +enum class TunnelState : int { + kIdle = 0, + kConnecting = 1, + kReady = 2, + kClosed = 3, + kError = 4, +}; + +const char *tunnelStateName(TunnelState s) { + switch (s) { + case TunnelState::kIdle: return "idle"; + case TunnelState::kConnecting: return "connecting"; + case TunnelState::kReady: return "ready"; + case TunnelState::kClosed: return "closed"; + case TunnelState::kError: return "error"; + } + return "idle"; +} + +std::mutex g_mutex; +std::shared_ptr g_ddk; +std::unique_ptr g_server; +std::unique_ptr g_tunnel; +napi_threadsafe_function g_tunnelTsfn = nullptr; +TunnelState g_tunnelState = TunnelState::kIdle; +std::string g_tunnelMessage; + +struct TunnelEvent { + char state[16]; + char *message; // malloc'd; ownership passes to the JS callback +}; + +void tunnelEventOnJs(napi_env env, napi_value jsCallback, void * /*context*/, void *rawData) { + auto *event = static_cast(rawData); + if (env != nullptr && jsCallback != nullptr) { + napi_value stateVal, msgVal, undefined; + napi_create_string_utf8(env, event->state, NAPI_AUTO_LENGTH, &stateVal); + napi_create_string_utf8(env, event->message ? event->message : "", NAPI_AUTO_LENGTH, &msgVal); + napi_get_undefined(env, &undefined); + napi_value argv[2] = { stateVal, msgVal }; + napi_call_function(env, undefined, jsCallback, 2, argv, nullptr); + } + if (event->message) free(event->message); + delete event; +} + +void setFieldInt(napi_env env, napi_value obj, const char *name, int64_t v) { + napi_value val; + napi_create_int64(env, v, &val); + napi_set_named_property(env, obj, name, val); +} + +void setFieldStr(napi_env env, napi_value obj, const char *name, const char *v) { + napi_value val; + napi_create_string_utf8(env, v ? v : "", NAPI_AUTO_LENGTH, &val); + napi_set_named_property(env, obj, name, val); +} + +std::string getStringArg(napi_env env, napi_value value) { + size_t len = 0; + if (napi_get_value_string_utf8(env, value, nullptr, 0, &len) != napi_ok || len > 64 * 1024) { + return {}; + } + std::string out(len, '\0'); + size_t copied = 0; + napi_get_value_string_utf8(env, value, out.data(), out.size() + 1, &copied); + out.resize(copied); + return out; +} + +// ============================================================ +// Server +// ============================================================ + +napi_value StartServer(napi_env env, napi_callback_info info) { + napi_value result; + napi_create_object(env, &result); + std::lock_guard lock(g_mutex); + + if (g_server && g_server->IsRunning()) { + setFieldInt(env, result, "code", 0); + setFieldInt(env, result, "port", g_server->BoundPort()); + return result; + } + if (g_ddk == nullptr) { + g_ddk = LoadDdk(); + if (g_ddk == nullptr) { + setFieldInt(env, result, "code", -1); + setFieldStr(env, result, "error", "USB DDK 不可用"); + return result; + } + } + g_server = std::make_unique(g_ddk); + std::string error; + const int port = g_server->Start(&error); + if (port < 0) { + g_server.reset(); + setFieldInt(env, result, "code", -1); + setFieldStr(env, result, "error", error.c_str()); + return result; + } + setFieldInt(env, result, "code", 0); + setFieldInt(env, result, "port", port); + return result; +} + +napi_value StopServer(napi_env env, napi_callback_info info) { + napi_value result; + napi_create_object(env, &result); + std::lock_guard lock(g_mutex); + + if (g_tunnel) { + g_tunnel->Stop(); + g_tunnel.reset(); + } + if (g_tunnelTsfn != nullptr) { + napi_release_threadsafe_function(g_tunnelTsfn, napi_tsfn_release); + g_tunnelTsfn = nullptr; + } + g_tunnelState = TunnelState::kIdle; + g_tunnelMessage.clear(); + if (g_server) { + g_server->Stop(); + g_server.reset(); + } + setFieldInt(env, result, "code", 0); + return result; +} + +napi_value AddDevice(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value args[3]; + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + + napi_value result; + napi_create_object(env, &result); + setFieldInt(env, result, "code", -1); + + int32_t busNum = 0, devAddr = 0; + std::string name; + if (argc >= 2) { + napi_get_value_int32(env, args[0], &busNum); + napi_get_value_int32(env, args[1], &devAddr); + } + if (argc >= 3) { + name = getStringArg(env, args[2]); + } + if (busNum <= 0 || devAddr <= 0) { + setFieldStr(env, result, "error", "invalid busNum/devAddress"); + return result; + } + + std::lock_guard lock(g_mutex); + if (!g_server || !g_server->IsRunning()) { + setFieldStr(env, result, "error", "server not running"); + return result; + } + std::string error; + if (!g_server->AddDevice(static_cast(busNum), static_cast(devAddr), + name, &error)) { + setFieldStr(env, result, "error", error.c_str()); + return result; + } + const std::vector list = g_server->devices(); + for (const auto &dev : list) { + if (dev.busNum == static_cast(busNum) && + dev.devAddress == static_cast(devAddr)) { + setFieldInt(env, result, "code", 0); + setFieldStr(env, result, "busId", dev.busId.c_str()); + setFieldInt(env, result, "vendorId", dev.vendorId); + setFieldInt(env, result, "productId", dev.productId); + setFieldInt(env, result, "interfaces", static_cast(dev.interfaces.size())); + setFieldInt(env, result, "hasIsochronous", dev.hasIsochronous ? 1 : 0); + break; + } + } + return result; +} + +napi_value RemoveDevice(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + + napi_value result; + napi_create_object(env, &result); + if (argc < 1) { + setFieldInt(env, result, "code", -1); + return result; + } + const std::string busId = getStringArg(env, args[0]); + std::lock_guard lock(g_mutex); + if (g_server) { + g_server->RemoveDevice(busId); + } + setFieldInt(env, result, "code", 0); + return result; +} + +// ============================================================ +// Tunnel +// ============================================================ + +napi_value StartTunnel(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + + napi_value result; + napi_create_object(env, &result); + setFieldInt(env, result, "code", -1); + if (argc < 2 || args[0] == nullptr) { + setFieldStr(env, result, "error", "invalid arguments"); + return result; + } + + TunnelConfig config; + { + napi_value v; + if (napi_get_named_property(env, args[0], "host", &v) == napi_ok) config.host = getStringArg(env, v); + if (napi_get_named_property(env, args[0], "token", &v) == napi_ok) config.sessionToken = getStringArg(env, v); + if (napi_get_named_property(env, args[0], "busId", &v) == napi_ok) config.localBusId = getStringArg(env, v); + if (napi_get_named_property(env, args[0], "clientCertPem", &v) == napi_ok) config.clientCertPem = getStringArg(env, v); + if (napi_get_named_property(env, args[0], "clientKeyPem", &v) == napi_ok) config.clientKeyPem = getStringArg(env, v); + if (napi_get_named_property(env, args[0], "serverCertPem", &v) == napi_ok) config.serverCertPem = getStringArg(env, v); + int32_t port = 0; + if (napi_get_named_property(env, args[0], "port", &v) == napi_ok) { + napi_get_value_int32(env, v, &port); + if (port > 0) config.port = static_cast(port); + } + } + + std::lock_guard lock(g_mutex); + + if (!g_server || !g_server->IsRunning()) { + setFieldStr(env, result, "error", "USB/IP server not running"); + return result; + } + if (g_tunnel) { + setFieldStr(env, result, "error", "tunnel already running"); + return result; + } + config.localPort = static_cast(g_server->BoundPort()); + const bool valid = !config.host.empty() && config.port != 0 && + !config.sessionToken.empty() && !config.clientCertPem.empty() && + !config.clientKeyPem.empty() && !config.serverCertPem.empty() && + !config.localBusId.empty() && config.localPort != 0; + if (!valid) { + setFieldStr(env, result, "error", "tunnel configuration incomplete"); + return result; + } + + // JS state callback (threadsafe; the tunnel reports from its own thread). + if (g_tunnelTsfn != nullptr) { + napi_release_threadsafe_function(g_tunnelTsfn, napi_tsfn_release); + g_tunnelTsfn = nullptr; + } + napi_value resName; + napi_create_string_utf8(env, "UsbIpTunnelState", NAPI_AUTO_LENGTH, &resName); + if (napi_create_threadsafe_function(env, args[1], nullptr, resName, 16, 1, + nullptr, nullptr, nullptr, + tunnelEventOnJs, &g_tunnelTsfn) != napi_ok) { + g_tunnelTsfn = nullptr; + setFieldStr(env, result, "error", "failed to create state callback"); + return result; + } + + Server *server = g_server.get(); + config.onLocalBound = [server](uint16_t port) { server->AuthorizePort(port); }; + + g_tunnelState = TunnelState::kConnecting; + g_tunnelMessage.clear(); + g_tunnel = std::make_unique(std::move(config)); + g_tunnel->Start([](const char *state, const char *message) { + // Runs on the tunnel thread: update the pollable snapshot and + // forward to JS. + if (strcmp(state, "connecting") == 0) g_tunnelState = TunnelState::kConnecting; + else if (strcmp(state, "ready") == 0) g_tunnelState = TunnelState::kReady; + else if (strcmp(state, "closed") == 0) g_tunnelState = TunnelState::kClosed; + else g_tunnelState = TunnelState::kError; + g_tunnelMessage = message ? message : ""; + + auto *event = new (std::nothrow) TunnelEvent{}; + if (event == nullptr) return; + strncpy(event->state, state, sizeof(event->state) - 1); + const size_t len = g_tunnelMessage.size(); + event->message = static_cast(malloc(len + 1)); + if (event->message != nullptr) { + memcpy(event->message, g_tunnelMessage.c_str(), len + 1); + } + if (g_tunnelTsfn != nullptr) { + napi_call_threadsafe_function(g_tunnelTsfn, event, napi_tsfn_nonblocking); + } else { + if (event->message) free(event->message); + delete event; + } + }); + + setFieldInt(env, result, "code", 0); + return result; +} + +napi_value StopTunnel(napi_env env, napi_callback_info info) { + napi_value result; + napi_create_object(env, &result); + std::lock_guard lock(g_mutex); + if (g_tunnel) { + g_tunnel->Stop(); + g_tunnel.reset(); + } + if (g_tunnelTsfn != nullptr) { + napi_release_threadsafe_function(g_tunnelTsfn, napi_tsfn_release); + g_tunnelTsfn = nullptr; + } + g_tunnelState = TunnelState::kIdle; + g_tunnelMessage.clear(); + setFieldInt(env, result, "code", 0); + return result; +} + +napi_value TunnelStateQuery(napi_env env, napi_callback_info info) { + napi_value result; + napi_create_object(env, &result); + std::lock_guard lock(g_mutex); + setFieldStr(env, result, "state", tunnelStateName(g_tunnelState)); + setFieldStr(env, result, "message", g_tunnelMessage.c_str()); + return result; +} + +} // namespace + +void UsbIpNapi_Init(napi_env env, napi_value exports) { + napi_value obj; + napi_create_object(env, &obj); + + napi_property_descriptor methods[] = { + { "startServer", nullptr, StartServer, nullptr, nullptr, nullptr, napi_default, nullptr }, + { "stopServer", nullptr, StopServer, nullptr, nullptr, nullptr, napi_default, nullptr }, + { "addDevice", nullptr, AddDevice, nullptr, nullptr, nullptr, napi_default, nullptr }, + { "removeDevice", nullptr, RemoveDevice, nullptr, nullptr, nullptr, napi_default, nullptr }, + { "startTunnel", nullptr, StartTunnel, nullptr, nullptr, nullptr, napi_default, nullptr }, + { "stopTunnel", nullptr, StopTunnel, nullptr, nullptr, nullptr, napi_default, nullptr }, + { "tunnelState", nullptr, TunnelStateQuery, nullptr, nullptr, nullptr, napi_default, nullptr }, + }; + + napi_define_properties(env, obj, sizeof(methods) / sizeof(methods[0]), methods); + napi_set_named_property(env, exports, "UsbIp", obj); + + LOGI("[%{public}s] UsbIp NAPI registered", LOG_TAG); +} + +} // namespace usbip diff --git a/nativelib/src/main/cpp/usbip_napi.h b/nativelib/src/main/cpp/usbip_napi.h new file mode 100644 index 0000000..a5e72e4 --- /dev/null +++ b/nativelib/src/main/cpp/usbip_napi.h @@ -0,0 +1,24 @@ +/* + * Moonlight for HarmonyOS + * Copyright (C) 2025 Moonlight/AlkaidLab + * + * usbip_napi - NAPI surface for the USB/IP reverse tunnel. + * + * Exposes the usbip::Server (in-app USB/IP exporter over the USB DDK) and + * usbip::Tunnel (TLS reverse tunnel to Sunshine) as one "UsbIp" object and + * wires the tunnel's reserved loopback source port into the server's + * listener authorization. + */ + +#ifndef USBIP_NAPI_H +#define USBIP_NAPI_H + +#include + +namespace usbip { + +void UsbIpNapi_Init(napi_env env, napi_value exports); + +} // namespace usbip + +#endif // USBIP_NAPI_H diff --git a/nativelib/src/main/cpp/usbip_server.cpp b/nativelib/src/main/cpp/usbip_server.cpp new file mode 100644 index 0000000..8fa301d --- /dev/null +++ b/nativelib/src/main/cpp/usbip_server.cpp @@ -0,0 +1,875 @@ +/* + * Moonlight for HarmonyOS + * Copyright (C) 2025 Moonlight/AlkaidLab + * + * usbip_server.cpp - USB/IP server over the HarmonyOS USB DDK. + * + * USB/IP 1.1.1 wire protocol constants and framing are ported from the + * Linux kernel usbip protocol (Documentation/usb/usbip_protocol.rst) and + * cross-checked against Sunshine's loopback_usbip_bridge. + */ + +#include "usbip_server.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#define LOG_TAG "UsbIpServer" +#define LOGI(...) OH_LOG_INFO(LOG_APP, __VA_ARGS__) +#define LOGW(...) OH_LOG_WARN(LOG_APP, __VA_ARGS__) +#define LOGE(...) OH_LOG_ERROR(LOG_APP, __VA_ARGS__) + +namespace usbip { + +// ============================================================ +// USB/IP wire constants (all big-endian on the wire) +// ============================================================ + +namespace wire { + +constexpr uint16_t kVersion = 0x0111; +constexpr uint16_t kOpReqDevlist = 0x8005; +constexpr uint16_t kOpRepDevlist = 0x0005; +constexpr uint16_t kOpReqImport = 0x8003; +constexpr uint16_t kOpRepImport = 0x0003; +constexpr int kOpCommonSize = 8; // version + code + status +constexpr int kUsbDeviceStructSize = 0x138; // 312 bytes +constexpr int kPduHeaderSize = 48; +constexpr int kBusIdSize = 32; + +constexpr uint32_t kCmdSubmit = 0x00000001; +constexpr uint32_t kCmdUnlink = 0x00000002; +constexpr uint32_t kRetSubmit = 0x00000003; +constexpr uint32_t kRetUnlink = 0x00000004; + +constexpr int32_t kUsbErrIo = -5; // -EIO +constexpr int32_t kUsbErrNoent = -2; // -ENOENT +constexpr int32_t kUsbErrOverflow = -75; // -EOVERFLOW +constexpr int32_t kUsbErrTimedout = -110;// -ETIMEDOUT + +// DDK error codes (usb_ddk_types.h) +constexpr int32_t kDdkTimeout = 27400004; + +// 312-byte usbip_usb_device layout offsets +constexpr int kBusidOffset = 0x100; // busid[32] +constexpr int kBusnumOffset = 0x120; +constexpr int kDevnumOffset = 0x124; +constexpr int kSpeedOffset = 0x128; +constexpr int kIdVendorOffset = 0x12C; +constexpr int kIdProductOffset = 0x12E; +constexpr int kBcdDeviceOffset = 0x130; +constexpr int kClassOffset = 0x132; +constexpr int kSubclassOffset = 0x133; +constexpr int kProtocolOffset = 0x134; +constexpr int kConfigValueOffset = 0x135; +constexpr int kNumConfigsOffset = 0x136; +constexpr int kNumInterfacesOffset = 0x137; + +// 48-byte PDU header offsets +constexpr int kCmdOffset = 0; +constexpr int kSeqnumOffset = 4; +constexpr int kDirectionOffset = 12; +constexpr int kEpOffset = 16; +constexpr int kFlagsOrUnlinkTargetOffset = 20; +constexpr int kLenOrActualOffset = 24; +constexpr int kIntervalOffset = 36; +constexpr int kSetupOffset = 40; // setup[8] + +void appendU16(std::vector &out, uint16_t v) { + out.push_back((v >> 8) & 0xFF); + out.push_back(v & 0xFF); +} +void appendU32(std::vector &out, uint32_t v) { + out.push_back((v >> 24) & 0xFF); + out.push_back((v >> 16) & 0xFF); + out.push_back((v >> 8) & 0xFF); + out.push_back(v & 0xFF); +} +void appendI32(std::vector &out, int32_t v) { + appendU32(out, static_cast(v)); +} + +uint16_t readU16(const uint8_t *p) { + return (uint16_t(p[0]) << 8) | uint16_t(p[1]); +} +uint32_t readU32(const uint8_t *p) { + return (uint32_t(p[0]) << 24) | (uint32_t(p[1]) << 16) | + (uint32_t(p[2]) << 8) | uint32_t(p[3]); +} +int32_t readI32(const uint8_t *p) { + return static_cast(readU32(p)); +} + +bool writeAll(int fd, const uint8_t *data, size_t len) { + size_t off = 0; + while (off < len) { + const ssize_t n = ::write(fd, data + off, len - off); + if (n <= 0) return false; + off += static_cast(n); + } + return true; +} + +bool writeVec(int fd, const std::vector &v) { + return writeAll(fd, v.data(), v.size()); +} + +// Blocking exact read. Only call after poll() reported readability (the +// tunnel peer never trickles, so this cannot wedge). +bool readAll(int fd, uint8_t *buf, size_t len) { + size_t off = 0; + while (off < len) { + const ssize_t n = ::read(fd, buf + off, len - off); + if (n <= 0) return false; + off += static_cast(n); + } + return true; +} + +// True when the socket has data within timeoutMs (0 = non-blocking probe). +bool waitReadable(int fd, int timeoutMs) { + pollfd pfd{fd, POLLIN, 0}; + return ::poll(&pfd, 1, timeoutMs) > 0 && (pfd.revents & (POLLIN | POLLHUP | POLLERR)); +} + +// The 312-byte usbip_usb_device struct for a registered device. +std::vector buildDeviceEntry(const DeviceInfo &dev) { + std::vector entry(kUsbDeviceStructSize, 0); + const std::string busid = dev.busId.substr(0, kBusIdSize - 1); + std::memcpy(entry.data() + kBusidOffset, busid.c_str(), busid.size()); + const auto putU16 = [&](int off, uint16_t v) { + entry[off] = (v >> 8) & 0xFF; + entry[off + 1] = v & 0xFF; + }; + const auto putU32 = [&](int off, uint32_t v) { + entry[off] = (v >> 24) & 0xFF; + entry[off + 1] = (v >> 16) & 0xFF; + entry[off + 2] = (v >> 8) & 0xFF; + entry[off + 3] = v & 0xFF; + }; + putU32(kBusnumOffset, dev.busNum); + putU32(kDevnumOffset, dev.devAddress); + putU32(kSpeedOffset, dev.speed); + putU16(kIdVendorOffset, dev.vendorId); + putU16(kIdProductOffset, dev.productId); + putU16(kBcdDeviceOffset, dev.bcdDevice); + entry[kClassOffset] = dev.klass; + entry[kSubclassOffset] = dev.subclass; + entry[kProtocolOffset] = dev.protocol; + entry[kConfigValueOffset] = 1; + entry[kNumConfigsOffset] = dev.numConfigurations; + entry[kNumInterfacesOffset] = static_cast(dev.interfaces.size()); + return entry; +} + +} // namespace wire + +// ============================================================ +// DDK loader +// ============================================================ + +std::shared_ptr LoadDdk() { + auto api = std::make_shared(); + api->handle = dlopen("libusb_ndk.z.so", RTLD_LAZY); + if (api->handle == nullptr) { + LOGE("[%{public}s] dlopen libusb_ndk.z.so failed: %{public}s", LOG_TAG, dlerror()); + return nullptr; + } + auto load = [&](const char *name) -> void * { + return dlsym(api->handle, name); + }; + api->Init = reinterpret_cast(load("OH_Usb_Init")); + api->GetDeviceDescriptor = reinterpret_cast( + load("OH_Usb_GetDeviceDescriptor")); + api->GetConfigDescriptor = + reinterpret_cast( + load("OH_Usb_GetConfigDescriptor")); + api->FreeConfigDescriptor = + reinterpret_cast(load("OH_Usb_FreeConfigDescriptor")); + api->ClaimInterface = + reinterpret_cast(load("OH_Usb_ClaimInterface")); + api->ReleaseInterface = + reinterpret_cast(load("OH_Usb_ReleaseInterface")); + api->SelectInterfaceSetting = + reinterpret_cast(load("OH_Usb_SelectInterfaceSetting")); + api->SendControlReadRequest = reinterpret_cast( + load("OH_Usb_SendControlReadRequest")); + api->SendControlWriteRequest = reinterpret_cast( + load("OH_Usb_SendControlWriteRequest")); + api->SendPipeRequest = reinterpret_cast( + load("OH_Usb_SendPipeRequest")); + api->CreateDeviceMemMap = + reinterpret_cast( + load("OH_Usb_CreateDeviceMemMap")); + api->DestroyDeviceMemMap = + reinterpret_cast(load("OH_Usb_DestroyDeviceMemMap")); + + if (api->GetDeviceDescriptor == nullptr || api->ClaimInterface == nullptr || + api->SendPipeRequest == nullptr || api->CreateDeviceMemMap == nullptr) { + LOGE("[%{public}s] USB DDK core functions missing", LOG_TAG); + dlclose(api->handle); + return nullptr; + } + return api; +} + +namespace { + +// The DDK requires OH_Usb_Init before any other call. It may already have +// been called by usb_ddk_poller in this process; tolerate that by probing a +// read-only call afterwards. Init is never released here — another module +// may still hold DDK resources. +bool ensureDdkInited(const std::shared_ptr &ddk) { + static std::once_flag once; + static bool ok = false; + std::call_once(once, [&] { + if (ddk->Init == nullptr) { + ok = true; // old DDK without explicit init requirement + return; + } + const int32_t rc = ddk->Init(); + if (rc == 0) { + ok = true; + return; + } + // Already-initialized (or another quirk): accept when descriptors + // are still readable. deviceId 0 is never valid, so any result other + // than INVALID_OPERATION means the DDK layer is up. + UsbDeviceDescriptor probe{}; + ok = ddk->GetDeviceDescriptor != nullptr && + ddk->GetDeviceDescriptor(0, &probe) != 27400002; + LOGW("[%{public}s] OH_Usb_Init rc=%{public}d, probe-ok=%{public}d", LOG_TAG, rc, ok ? 1 : 0); + }); + return ok; +} + +} // namespace + +// ============================================================ +// Server +// ============================================================ + +Server::Server(std::shared_ptr ddk) : ddk_(std::move(ddk)) {} + +Server::~Server() { + Stop(); +} + +std::vector Server::devices() const { + std::lock_guard lock(devicesMutex_); + return devices_; +} + +void Server::AuthorizePort(uint16_t port) noexcept { + authorizedPort_.store(port); +} + +int Server::Start(std::string *error) { + if (ddk_ == nullptr) { + if (error) *error = "USB DDK unavailable"; + return -1; + } + if (!ensureDdkInited(ddk_)) { + if (error) *error = "OH_Usb_Init failed"; + return -1; + } + + listenFd_ = ::socket(AF_INET, SOCK_STREAM, 0); + if (listenFd_ < 0) { + if (error) *error = "socket() failed"; + return -1; + } + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + addr.sin_port = 0; // ephemeral + if (::bind(listenFd_, reinterpret_cast(&addr), sizeof(addr)) < 0) { + if (error) *error = "bind() failed"; + ::close(listenFd_); + listenFd_ = -1; + return -1; + } + if (::listen(listenFd_, 1) < 0) { + if (error) *error = "listen() failed"; + ::close(listenFd_); + listenFd_ = -1; + return -1; + } + sockaddr_in bound{}; + socklen_t boundLen = sizeof(bound); + ::getsockname(listenFd_, reinterpret_cast(&bound), &boundLen); + boundPort_ = ntohs(bound.sin_port); + + running_.store(true); + acceptThread_ = std::thread([this]() { AcceptLoop(); }); + LOGI("[%{public}s] listening on 127.0.0.1:%{public}d", LOG_TAG, boundPort_); + return boundPort_; +} + +void Server::Stop() noexcept { + if (!running_.exchange(false)) return; + if (listenFd_ >= 0) { + ::shutdown(listenFd_, SHUT_RDWR); + ::close(listenFd_); + listenFd_ = -1; + } + if (acceptThread_.joinable()) { + acceptThread_.join(); + } + authorizedPort_.store(-1); +} + +bool Server::AddDevice(uint32_t busNum, uint32_t devAddress, const std::string &name, + std::string *error) { + // Official encoding (usb_ddk_types.h, Usb_NonRootHubArray). + const uint64_t deviceId = (uint64_t(busNum) << 32) | uint64_t(devAddress); + + DeviceInfo info; + info.deviceId = deviceId; + info.busNum = busNum; + info.devAddress = devAddress; + info.busId = "1-" + std::to_string(devAddress); + info.name = name; + if (!ReadDescriptors(&info, error)) { + return false; + } + + std::lock_guard lock(devicesMutex_); + for (auto it = devices_.begin(); it != devices_.end();) { + if (it->deviceId == deviceId || it->busId == info.busId) { + it = devices_.erase(it); + } else { + ++it; + } + } + devices_.push_back(info); + LOGI("[%{public}s] registered %{public}s (%{public}s) vid=%{public}04x pid=%{public}04x " + "ifaces=%{public}zu iso=%{public}d", + LOG_TAG, info.busId.c_str(), info.name.c_str(), info.vendorId, info.productId, + info.interfaces.size(), info.hasIsochronous ? 1 : 0); + return true; +} + +void Server::RemoveDevice(const std::string &busId) { + std::lock_guard lock(devicesMutex_); + for (auto it = devices_.begin(); it != devices_.end();) { + if (it->busId == busId) { + LOGI("[%{public}s] unregistered %{public}s", LOG_TAG, busId.c_str()); + it = devices_.erase(it); + } else { + ++it; + } + } +} + +bool Server::ReadDescriptors(DeviceInfo *info, std::string *error) { + UsbDeviceDescriptor desc{}; + if (ddk_->GetDeviceDescriptor(info->deviceId, &desc) != 0) { + if (error) { + *error = "GetDeviceDescriptor failed (USB 权限未授予或设备已拔出)"; + } + return false; + } + info->vendorId = desc.idVendor; + info->productId = desc.idProduct; + info->bcdDevice = desc.bcdDevice; + info->klass = desc.bDeviceClass; + info->subclass = desc.bDeviceSubClass; + info->protocol = desc.bDeviceProtocol; + info->numConfigurations = desc.bNumConfigurations ? desc.bNumConfigurations : 1; + + if (ddk_->GetConfigDescriptor == nullptr) { + return true; + } + UsbDdkConfigDescriptor *config = nullptr; + if (ddk_->GetConfigDescriptor(info->deviceId, 0, &config) != 0 || config == nullptr) { + // The host cannot attach without the interface list; fail loudly + // instead of exporting a ghost device. + if (error) *error = "GetConfigDescriptor failed"; + return false; + } + for (uint32_t ii = 0; ii < config->configDescriptor.bNumInterfaces; ++ii) { + const UsbDdkInterface &ddkIface = config->interface[ii]; + if (ddkIface.numAltsetting == 0 || ddkIface.altsetting == nullptr) { + continue; + } + const UsbDdkInterfaceDescriptor &ddkAlt = ddkIface.altsetting[0]; + DeviceInterface iface{}; + iface.number = ddkAlt.interfaceDescriptor.bInterfaceNumber; + iface.altSetting = ddkAlt.interfaceDescriptor.bAlternateSetting; + iface.klass = ddkAlt.interfaceDescriptor.bInterfaceClass; + iface.subclass = ddkAlt.interfaceDescriptor.bInterfaceSubClass; + iface.protocol = ddkAlt.interfaceDescriptor.bInterfaceProtocol; + for (uint32_t jj = 0; jj < ddkAlt.interfaceDescriptor.bNumEndpoints; ++jj) { + const UsbDdkEndpointDescriptor &ddkEp = ddkAlt.endPoint[jj]; + DeviceEndpoint ep; + ep.address = ddkEp.endpointDescriptor.bEndpointAddress; + ep.attributes = ddkEp.endpointDescriptor.bmAttributes; + ep.maxPacketSize = ddkEp.endpointDescriptor.wMaxPacketSize; + ep.interval = ddkEp.endpointDescriptor.bInterval; + iface.endpoints.push_back(ep); + // attributes & 0x03 == 0x01 means isochronous. + if ((ep.attributes & 0x03) == 0x01) { + info->hasIsochronous = true; + } + } + info->interfaces.push_back(iface); + } + ddk_->FreeConfigDescriptor(config); + return true; +} + +void Server::AcceptLoop() { + while (running_.load()) { + sockaddr_in peer{}; + socklen_t peerLen = sizeof(peer); + const int fd = ::accept(listenFd_, reinterpret_cast(&peer), &peerLen); + if (fd < 0) break; + + // Loopback + pre-authorized source port only: the exported device + // must not be reachable by unrelated local processes. + const uint16_t peerPort = ntohs(peer.sin_port); + const bool fromLoopback = (ntohl(peer.sin_addr.s_addr) >> 24) == 127; + const int authorized = authorizedPort_.load(); + if (!fromLoopback || authorized < 0 || peerPort != static_cast(authorized)) { + LOGW("[%{public}s] rejected connection from 127.0.0.1:%{public}u (authorized=%{public}d)", + LOG_TAG, peerPort, authorized); + ::close(fd); + continue; + } + authorizedPort_.store(-1); // one shot: this connection is the tunnel + + HandleConnection(fd); + ::close(fd); + } +} + +void Server::HandleConnection(int fd) { + using namespace wire; + + // Expect OP_REQ_DEVLIST first (the tunnel's bridge imports directly). + uint8_t header[kOpCommonSize]; + if (!readAll(fd, header, sizeof(header))) return; + if (readU16(header) != kVersion) return; + const uint16_t code = readU16(header + 2); + if (code == kOpReqDevlist) { + std::vector list = devices(); + std::vector reply; + appendU16(reply, kVersion); + appendU16(reply, kOpRepDevlist); + appendU32(reply, 0); // status OK + appendU32(reply, static_cast(list.size())); + for (const auto &dev : list) { + const std::vector entry = buildDeviceEntry(dev); + reply.insert(reply.end(), entry.begin(), entry.end()); + // usbip_usb_interface: class/subclass/protocol + pad. + for (const auto &iface : dev.interfaces) { + uint8_t ifaceEntry[4] = { iface.klass, iface.subclass, iface.protocol, 0 }; + reply.insert(reply.end(), ifaceEntry, ifaceEntry + 4); + } + } + if (!writeVec(fd, reply)) return; + + if (!waitReadable(fd, 1000)) return; // list-only client hangs up + if (!readAll(fd, header, sizeof(header))) return; + if (readU16(header) != kVersion || readU16(header + 2) != kOpReqImport) return; + } else if (code != kOpReqImport) { + return; + } + + // OP_REQ_IMPORT: 8-byte op_common + 32-byte busid. + uint8_t busidBuf[kBusIdSize] = {}; + if (!readAll(fd, busidBuf, sizeof(busidBuf))) return; + const std::string busid(reinterpret_cast(busidBuf), + ::strnlen(reinterpret_cast(busidBuf), sizeof(busidBuf))); + + DeviceInfo selected{}; + bool found = false; + for (const auto &dev : devices()) { + if (dev.busId == busid) { + selected = dev; + found = true; + break; + } + } + + if (!found) { + std::vector reply; + appendU16(reply, kVersion); + appendU16(reply, kOpRepImport); + appendU32(reply, 1); // ST_NA + writeVec(fd, reply); + return; + } + + ServeImport(fd, selected); +} + +bool Server::ServeImport(int clientFd, const DeviceInfo &device) { + using namespace wire; + + // Claim every interface; the host owns the whole device while attached. + std::vector handles; + for (const auto &iface : device.interfaces) { + uint64_t handle = 0; + int32_t rc = ddk_->ClaimInterface(device.deviceId, iface.number, &handle); + for (int attempt = 1; rc != 0 && attempt <= 3; ++attempt) { + // usbManager may still be releasing the interface (same race the + // DDK poller works around). + usleep(100 * 1000); + rc = ddk_->ClaimInterface(device.deviceId, iface.number, &handle); + } + if (rc != 0) { + LOGE("[%{public}s] ClaimInterface(%{public}u) failed: %{public}d", LOG_TAG, + iface.number, rc); + for (uint64_t h : handles) { + if (ddk_->ReleaseInterface) ddk_->ReleaseInterface(h); + } + std::vector reply; + appendU16(reply, kVersion); + appendU16(reply, kOpRepImport); + appendU32(reply, 1); // ST_NA + writeVec(clientFd, reply); + return false; + } + handles.push_back(handle); + } + if (handles.empty()) { + // Descriptor-less device: nothing the host can drive. + std::vector reply; + appendU16(reply, kVersion); + appendU16(reply, kOpRepImport); + appendU32(reply, 1); + writeVec(clientFd, reply); + return false; + } + + std::vector reply; + appendU16(reply, kVersion); + appendU16(reply, kOpRepImport); + appendU32(reply, 0); // ST_OK + const std::vector entry = buildDeviceEntry(device); + reply.insert(reply.end(), entry.begin(), entry.end()); + if (!writeVec(clientFd, reply)) { + for (uint64_t h : handles) { + if (ddk_->ReleaseInterface) ddk_->ReleaseInterface(h); + } + return false; + } + LOGI("[%{public}s] %{public}s imported (%{public}zu interfaces)", LOG_TAG, + device.busId.c_str(), handles.size()); + + UsbDeviceMemMap *mmap = nullptr; + const size_t kMmapSize = 64 * 1024; + if (ddk_->CreateDeviceMemMap(device.deviceId, kMmapSize, &mmap) != 0 || mmap == nullptr) { + LOGE("[%{public}s] CreateDeviceMemMap failed", LOG_TAG); + for (uint64_t h : handles) { + if (ddk_->ReleaseInterface) ddk_->ReleaseInterface(h); + } + return false; + } + + PumpUrbLoop(clientFd, device, handles, mmap); + + ddk_->DestroyDeviceMemMap(mmap); + for (uint64_t h : handles) { + if (ddk_->ReleaseInterface) ddk_->ReleaseInterface(h); + } + LOGI("[%{public}s] %{public}s released", LOG_TAG, device.busId.c_str()); + return true; +} + +void Server::PumpUrbLoop(int clientFd, const DeviceInfo &device, + const std::vector &handles, UsbDeviceMemMap *mmap) { + using namespace wire; + + // An interrupt/bulk IN URB can wait on the device indefinitely while the + // host may still send control/OUT traffic. DDK reads run in 200 ms + // slices; between slices we answer UNLINK for the head URB or suspend it + // to service queued PDUs, then resume. + constexpr int kInSliceMs = 200; + constexpr int kIdlePollMs = 500; + constexpr uint32_t kControlTimeoutMs = 5000; + constexpr size_t kMaxControlData = 4096; + constexpr size_t kMaxPendingIn = 16; + + // Endpoint address / interface number -> owning interface handle (pipe + // and class-control requests address a claimed interface, not the device). + std::map epHandles; + std::map ifaceHandles; + for (size_t i = 0; i < device.interfaces.size() && i < handles.size(); ++i) { + ifaceHandles[device.interfaces[i].number] = handles[i]; + for (const auto &ep : device.interfaces[i].endpoints) { + epHandles[ep.address] = handles[i]; + } + } + const uint64_t firstHandle = handles.front(); + const auto handleForEp = [&](uint8_t ep) -> uint64_t { + const auto it = epHandles.find(ep); + return it != epHandles.end() ? it->second : firstHandle; + }; + const auto handleForIface = [&](uint8_t iface) -> uint64_t { + const auto it = ifaceHandles.find(iface); + return it != ifaceHandles.end() ? it->second : firstHandle; + }; + + const auto sendRetSubmit = [&](uint32_t seqnum, int32_t status, int32_t actual, + const uint8_t *data = nullptr) { + std::vector ret(kPduHeaderSize + (actual > 0 ? actual : 0), 0); + appendU32(ret, kRetSubmit); + appendU32(ret, seqnum); + appendI32(ret, status); // [20] + appendI32(ret, actual); // [24] actual_length + appendI32(ret, 0); // [28] start_frame + appendI32(ret, -1); // [32] number_of_packets (not isochronous) + appendI32(ret, 0); // [36] error_count + if (actual > 0 && data != nullptr) { + std::memcpy(ret.data() + kPduHeaderSize, data, static_cast(actual)); + } + return writeVec(clientFd, ret); + }; + const auto sendRetUnlink = [&](uint32_t seqnum, int32_t status) { + std::vector ret(kPduHeaderSize, 0); + appendU32(ret, kRetUnlink); + appendU32(ret, seqnum); + appendI32(ret, status); // [20] + return writeVec(clientFd, ret); + }; + + struct PendingIn { + uint32_t seqnum = 0; + uint32_t endpoint = 0; + uint64_t handle = 0; + int32_t length = 0; + }; + std::deque pendingIn; + + enum class Drive { Completed, Suspended, Closed }; + + // Executes the head IN URB to completion (or suspension). Only called + // between DDK slices, so pendingIn mutations by the caller are safe. + const auto runHeadInUrb = [&]() -> Drive { + PendingIn &p = pendingIn.front(); + int deviceErrors = 0; + while (running_.load()) { + UsbRequestPipe pipe{}; + pipe.interfaceHandle = p.handle; + pipe.timeout = kInSliceMs; + pipe.endpoint = static_cast(p.endpoint & 0xFF); + mmap->offset = 0; + mmap->bufferLength = static_cast( + p.length > 0 && static_cast(p.length) <= mmap->size + ? p.length + : static_cast(mmap->size)); + mmap->transferedLength = 0; + const int32_t rc = ddk_->SendPipeRequest(&pipe, mmap); + + if (rc == 0) { + return sendRetSubmit(p.seqnum, 0, + static_cast(mmap->transferedLength), + mmap->address) + ? Drive::Completed + : Drive::Closed; + } + if (rc == kDdkTimeout) { + // Slice expired: answer an UNLINK of this URB, otherwise + // yield to whatever else the host queued. + if (waitReadable(clientFd, 0)) { + uint8_t peek[kPduHeaderSize]; + if (::recv(clientFd, peek, sizeof(peek), MSG_PEEK) < + static_cast(sizeof(peek))) { + return Drive::Closed; + } + if (readU32(peek + kCmdOffset) == kCmdUnlink && + readU32(peek + kFlagsOrUnlinkTargetOffset) == p.seqnum) { + uint8_t pdu[kPduHeaderSize]; + if (!readAll(clientFd, pdu, sizeof(pdu))) return Drive::Closed; + return sendRetUnlink(readU32(pdu + kSeqnumOffset), 0) + ? Drive::Completed + : Drive::Closed; + } + return Drive::Suspended; + } + continue; // no news from the host: keep waiting on the device + } + // Real device error (detached, babble...). A short burst is + // tolerated; a sustained one ends the URB and the connection so + // the tunnel surfaces the failure instead of black-holing URBs. + if (++deviceErrors < 3) { + usleep(20 * 1000); + continue; + } + LOGE("[%{public}s] IN ep 0x%{public}02x failed rc=%{public}d, closing", LOG_TAG, + pipe.endpoint, rc); + sendRetSubmit(p.seqnum, kUsbErrIo, 0); + return Drive::Closed; + } + // Stop() while a URB is outstanding: complete it as unlinked. + sendRetUnlink(p.seqnum, 0); + return Drive::Closed; + }; + + // Handles one 48-byte PDU already read off the socket. + // Returns false when the connection must close. + const auto processPdu = [&](const uint8_t pdu[kPduHeaderSize]) -> bool { + const uint32_t cmd = readU32(pdu + kCmdOffset); + const uint32_t seqnum = readU32(pdu + kSeqnumOffset); + const uint32_t direction = readU32(pdu + kDirectionOffset); + const uint32_t endpoint = readU32(pdu + kEpOffset); + + if (cmd == kCmdSubmit) { + const int32_t transferLength = readI32(pdu + kLenOrActualOffset); + (void)readU32(pdu + kFlagsOrUnlinkTargetOffset); // transfer_flags + (void)readI32(pdu + kIntervalOffset); // interval + + if (endpoint == 0) { + // Control transfer: setup[8] lives in the PDU itself. + UsbControlRequestSetup setup{}; + setup.bmRequestType = pdu[kSetupOffset]; + setup.bRequest = pdu[kSetupOffset + 1]; + setup.wValue = readU16(pdu + kSetupOffset + 2); + setup.wIndex = readU16(pdu + kSetupOffset + 4); + setup.wLength = readU16(pdu + kSetupOffset + 6); + + // Class requests carry the target interface in wIndex. + const uint64_t handle = + handleForIface(static_cast(setup.wIndex & 0xFF)); + + const bool isIn = (setup.bmRequestType & 0x80) != 0; + uint8_t data[kMaxControlData] = {}; + int32_t rc = 0; + int32_t actual = 0; + if (isIn) { + if (setup.wLength > kMaxControlData) { + return sendRetSubmit(seqnum, kUsbErrOverflow, 0); + } + uint32_t got = setup.wLength; + rc = ddk_->SendControlReadRequest(handle, &setup, kControlTimeoutMs, + data, &got); + actual = (rc == 0) ? static_cast(got) : 0; + if (rc == kDdkTimeout) rc = kUsbErrTimedout; + else if (rc != 0) rc = kUsbErrIo; + return sendRetSubmit(seqnum, rc, actual, data); + } + int32_t len = transferLength > 0 ? transferLength : 0; + if (static_cast(len) > kMaxControlData) { + return sendRetSubmit(seqnum, kUsbErrOverflow, 0); + } + if (len > 0 && !readAll(clientFd, data, static_cast(len))) { + return false; + } + rc = ddk_->SendControlWriteRequest(handle, &setup, kControlTimeoutMs, + data, static_cast(len)); + if (rc == kDdkTimeout) rc = kUsbErrTimedout; + else if (rc != 0) rc = kUsbErrIo; + return sendRetSubmit(seqnum, rc, rc == 0 ? len : 0); + } + + // Bulk / interrupt transfer. + const uint8_t epAddr = static_cast(endpoint & 0xFF); + const uint64_t handle = handleForEp(epAddr); + const bool isIn = (direction == 1); + + if (isIn) { + if (pendingIn.size() >= kMaxPendingIn) { + return sendRetSubmit(seqnum, kUsbErrIo, 0); + } + PendingIn p; + p.seqnum = seqnum; + p.endpoint = endpoint; + p.handle = handle; + p.length = transferLength; + pendingIn.push_back(std::move(p)); + return true; + } + + // OUT: body follows the header; send through the mmap in chunks + // (HID writes are tiny, but stay correct up to 1 MB). + int32_t len = transferLength > 0 ? transferLength : 0; + if (len > 1024 * 1024) { + return sendRetSubmit(seqnum, kUsbErrOverflow, 0); + } + std::vector data(static_cast(len)); + if (len > 0 && !readAll(clientFd, data.data(), data.size())) { + return false; + } + int32_t status = 0; + size_t sent = 0; + while (sent < data.size()) { + const size_t chunk = std::min(data.size() - sent, mmap->size); + std::memcpy(mmap->address, data.data() + sent, chunk); + mmap->offset = 0; + mmap->bufferLength = static_cast(chunk); + mmap->transferedLength = 0; + UsbRequestPipe pipe{}; + pipe.interfaceHandle = handle; + pipe.timeout = kControlTimeoutMs; + pipe.endpoint = epAddr; + const int32_t rc = ddk_->SendPipeRequest(&pipe, mmap); + if (rc != 0) { + status = (rc == kDdkTimeout) ? kUsbErrTimedout : kUsbErrIo; + break; + } + sent += chunk; + } + return sendRetSubmit(seqnum, status, + status == 0 ? static_cast(data.size()) : 0); + } + + if (cmd == kCmdUnlink) { + // Payload field at [20] names the URB to unlink. + const uint32_t target = readU32(pdu + kFlagsOrUnlinkTargetOffset); + for (auto it = pendingIn.begin(); it != pendingIn.end(); ++it) { + if (it->seqnum == target) { + // Safe to touch here: we only run between DDK slices. + pendingIn.erase(it); + return sendRetUnlink(seqnum, 0); + } + } + // Already completed: ENOENT, mirroring the kernel stub. + return sendRetUnlink(seqnum, kUsbErrNoent); + } + + LOGW("[%{public}s] unknown PDU cmd %{public}u, closing", LOG_TAG, cmd); + return false; + }; + + while (running_.load()) { + // Drive queued IN URBs; Suspended means a PDU is waiting and takes + // priority below before the head URB resumes. + if (!pendingIn.empty()) { + const Drive d = runHeadInUrb(); + if (d == Drive::Closed) return; + if (d == Drive::Completed) { + pendingIn.pop_front(); + continue; + } + // Suspended: the queued PDU is readable right now. + uint8_t pdu[kPduHeaderSize]; + if (!readAll(clientFd, pdu, sizeof(pdu))) return; + if (!processPdu(pdu)) return; + continue; + } + + if (!waitReadable(clientFd, kIdlePollMs)) continue; + uint8_t pdu[kPduHeaderSize]; + if (!readAll(clientFd, pdu, sizeof(pdu))) return; + if (!processPdu(pdu)) return; + } +} + +} // namespace usbip diff --git a/nativelib/src/main/cpp/usbip_server.h b/nativelib/src/main/cpp/usbip_server.h new file mode 100644 index 0000000..c9fab74 --- /dev/null +++ b/nativelib/src/main/cpp/usbip_server.h @@ -0,0 +1,162 @@ +/* + * Moonlight for HarmonyOS + * Copyright (C) 2025 Moonlight/AlkaidLab + * + * usbip_server - In-app USB/IP server backed by the HarmonyOS USB DDK. + * + * Speaks the standard USB/IP 1.1.1 server protocol on a loopback TCP port, + * exactly as the reverse tunnel expects: Moonlight's tunnel client connects + * to 127.0.0.1:, Sunshine's usbip-win2 attaches through it. + * + * Device model: OH_Usb_GetDevices() returns an empty list for normal apps, + * so devices are registered from the ArkTS usbManager layer as + * (busNum, devAddress) pairs. The DDK deviceId is derived with the official + * encoding (busNum << 32 | devAddress, see Usb_NonRootHubArray in + * usb_ddk_types.h) and validated via OH_Usb_GetDeviceDescriptor. + * + * Only the tunnel's own loopback source port may talk to the server + * (mirrors Android's NativeUsbIp.authorizeLocalConnection): other processes + * on the device must not be able to drive the exported USB device. + * + * Supported (v1 boundary, consistent with usbipd-win/usbip-win2): + * - OP_REQ_DEVLIST / OP_REQ_IMPORT handshake + * - Control transfers (via OH_Usb_SendControlRead/WriteRequest) + * - Bulk and interrupt transfers (via OH_Usb_SendPipeRequest) + * - URB submit / unlink with RET_SUBMIT / RET_UNLINK + * - One device per connection (USB/IP model) + * + * Not supported: isochronous endpoints (DDK does not expose them). + */ + +#ifndef USBIP_SERVER_H +#define USBIP_SERVER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace usbip { + +// ============================================================ +// USB DDK function pointers (dlopen'd, same pattern as usb_ddk_poller) +// ============================================================ + +struct DdkApi { + int32_t (*Init)(void) = nullptr; + void (*Release)(void) = nullptr; + int32_t (*GetDeviceDescriptor)(uint64_t, struct UsbDeviceDescriptor *) = nullptr; + int32_t (*GetConfigDescriptor)(uint64_t, uint8_t, struct UsbDdkConfigDescriptor **) = nullptr; + void (*FreeConfigDescriptor)(struct UsbDdkConfigDescriptor *) = nullptr; + int32_t (*ClaimInterface)(uint64_t, uint8_t, uint64_t *) = nullptr; + int32_t (*ReleaseInterface)(uint64_t) = nullptr; + int32_t (*SelectInterfaceSetting)(uint64_t, uint8_t) = nullptr; + int32_t (*SendControlReadRequest)(uint64_t, const struct UsbControlRequestSetup *, + uint32_t, uint8_t *, uint32_t *) = nullptr; + int32_t (*SendControlWriteRequest)(uint64_t, const struct UsbControlRequestSetup *, + uint32_t, const uint8_t *, uint32_t) = nullptr; + int32_t (*SendPipeRequest)(const struct UsbRequestPipe *, struct UsbDeviceMemMap *) = nullptr; + int32_t (*CreateDeviceMemMap)(uint64_t, size_t, struct UsbDeviceMemMap **) = nullptr; + void (*DestroyDeviceMemMap)(struct UsbDeviceMemMap *) = nullptr; + void *handle = nullptr; +}; + +// dlopen("libusb_ndk.z.so") + dlsym. Returns nullptr on failure. +std::shared_ptr LoadDdk(); + +// ============================================================ +// Device model (what DEVLIST reports) +// ============================================================ + +struct DeviceEndpoint { + uint8_t address = 0; // includes direction bit + uint8_t attributes = 0; // transfer type bits 0..2 + uint16_t maxPacketSize = 0; + uint8_t interval = 0; +}; + +struct DeviceInterface { + uint8_t number = 0; + uint8_t altSetting = 0; + uint8_t klass = 0; + uint8_t subclass = 0; + uint8_t protocol = 0; + std::vector endpoints; +}; + +struct DeviceInfo { + uint64_t deviceId = 0; // DDK device id: busNum << 32 | devAddress + uint32_t busNum = 0; + uint32_t devAddress = 0; + std::string busId; // synthetic "1-" for the wire protocol + std::string name; // display name from usbManager (logging only) + uint16_t vendorId = 0; + uint16_t productId = 0; + uint16_t bcdDevice = 0; + uint8_t klass = 0; + uint8_t subclass = 0; + uint8_t protocol = 0; + uint8_t numConfigurations = 1; + uint8_t speed = 3; // USB_SPEED_HIGH + std::vector interfaces; + bool hasIsochronous = false; +}; + +// ============================================================ +// Server — one thread, one listening socket, one device per connection +// ============================================================ + +class Server { +public: + explicit Server(std::shared_ptr ddk); + ~Server(); + + // Bind + listen on loopback (port 0 = ephemeral). Returns the bound port, + // or -1 with *error set. + int Start(std::string *error); + void Stop() noexcept; + bool IsRunning() const noexcept { return running_.load(); } + int BoundPort() const noexcept { return boundPort_; } + + // Register a usbManager device. Validates the derived DDK deviceId and + // reads its descriptors. Returns false with *error set when the device + // cannot be accessed through the DDK. + bool AddDevice(uint32_t busNum, uint32_t devAddress, const std::string &name, + std::string *error); + void RemoveDevice(const std::string &busId); + std::vector devices() const; + + // Restricts the next accepted connection to this loopback source port. + // The tunnel binds its local socket first and authorizes itself here. + void AuthorizePort(uint16_t port) noexcept; + +private: + void AcceptLoop(); + void HandleConnection(int clientFd); + bool ReadDescriptors(DeviceInfo *info, std::string *error); + bool ServeImport(int clientFd, const DeviceInfo &device); + void PumpUrbLoop(int clientFd, const DeviceInfo &device, + const std::vector &handles, struct UsbDeviceMemMap *mmap); + + std::shared_ptr ddk_; + int listenFd_ = -1; + int boundPort_ = 0; + std::atomic running_{false}; + std::atomic authorizedPort_{-1}; + std::thread acceptThread_; + mutable std::mutex devicesMutex_; + std::vector devices_; +}; + +} // namespace usbip + +#endif // USBIP_SERVER_H diff --git a/nativelib/src/main/cpp/usbip_tunnel.cpp b/nativelib/src/main/cpp/usbip_tunnel.cpp new file mode 100644 index 0000000..28e1c20 --- /dev/null +++ b/nativelib/src/main/cpp/usbip_tunnel.cpp @@ -0,0 +1,427 @@ +/* + * Moonlight for HarmonyOS + * Copyright (C) 2025 Moonlight/AlkaidLab + * + * usbip_tunnel.cpp - Reverse tunnel client, native port from moonlight-qt. + */ + +#include "usbip_tunnel.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define LOG_TAG "UsbIpTunnel" +#define LOGI(...) OH_LOG_INFO(LOG_APP, __VA_ARGS__) +#define LOGW(...) OH_LOG_WARN(LOG_APP, __VA_ARGS__) +#define LOGE(...) OH_LOG_ERROR(LOG_APP, __VA_ARGS__) + +namespace usbip { + +namespace { + +constexpr int kConnectTimeoutSec = 10; +constexpr int kHandshakeTimeoutSec = 15; +constexpr size_t kMaxHandshakeBytes = 4096; +constexpr size_t kIoBufferSize = 64 * 1024; + +bool WriteAllPlain(int fd, const uint8_t *data, size_t len) { + size_t off = 0; + while (off < len) { + const ssize_t n = ::write(fd, data + off, len - off); + if (n <= 0) return false; + off += static_cast(n); + } + return true; +} + +bool ReadAllPlain(int fd, uint8_t *buf, size_t len) { + size_t off = 0; + while (off < len) { + const ssize_t n = ::read(fd, buf + off, len - off); + if (n <= 0) return false; + off += static_cast(n); + } + return true; +} + +int ConnectTcp(const std::string &host, uint16_t port, int timeoutSec) { + const int fd = ::socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) return -1; + + // Non-blocking connect with timeout. + const int flags = fcntl(fd, F_GETFL, 0); + fcntl(fd, F_SETFL, flags | O_NONBLOCK); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + addr.sin_addr.s_addr = inet_addr(host.c_str()); + + const int rc = ::connect(fd, reinterpret_cast(&addr), sizeof(addr)); + if (rc < 0 && errno != EINPROGRESS) { + ::close(fd); + return -1; + } + if (rc != 0) { + pollfd pfd{fd, POLLOUT, 0}; + if (::poll(&pfd, 1, timeoutSec * 1000) <= 0) { + ::close(fd); + return -1; + } + int err = 0; + socklen_t errLen = sizeof(err); + getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errLen); + if (err != 0) { + ::close(fd); + return -1; + } + } + fcntl(fd, F_SETFL, flags); // back to blocking + + int nodelay = 1; + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)); + return fd; +} + +// Extracts "reason":"..." from a compact JSON error line (no parser needed +// for this fixed contract; result is truncated for the UI). +std::string extractReason(const std::string &line) { + const size_t key = line.find("\"reason\""); + if (key == std::string::npos) return line.substr(0, 120); + const size_t colon = line.find(':', key); + if (colon == std::string::npos) return line.substr(0, 120); + size_t start = line.find('"', colon); + if (start == std::string::npos) return line.substr(0, 120); + ++start; + const size_t end = line.find('"', start); + return line.substr(start, (end == std::string::npos ? line.size() : end) - start); +} + +} // namespace + +Tunnel::Tunnel(TunnelConfig config) : config_(std::move(config)) {} + +Tunnel::~Tunnel() { + Stop(); +} + +bool Tunnel::IsValid() const { + return !config_.host.empty() && config_.port != 0 && + !config_.sessionToken.empty() && !config_.clientCertPem.empty() && + !config_.clientKeyPem.empty() && !config_.serverCertPem.empty() && + !config_.localBusId.empty() && config_.localPort != 0; +} + +void Tunnel::Start(StateCallback onState) { + onState_ = std::move(onState); + running_.store(true); + finished_.store(false); + localFd_.store(-1); + remoteFd_.store(-1); + thread_ = std::thread([this]() { Run(); }); +} + +void Tunnel::Stop() noexcept { + running_.store(false); + WakeSockets(); + if (thread_.joinable()) { + thread_.join(); + } + if (sslCtx_ != nullptr) { + SSL_CTX_free(reinterpret_cast(sslCtx_)); + sslCtx_ = nullptr; + } +} + +void Tunnel::WakeSockets() noexcept { + // shutdown() is idempotent; it turns any blocking connect/read/write on + // these sockets into an error so Run() unwinds promptly. Run() owns the + // close() calls on its own copies. + const int local = localFd_.load(); + const int remote = remoteFd_.load(); + if (local >= 0) ::shutdown(local, SHUT_RDWR); + if (remote >= 0) ::shutdown(remote, SHUT_RDWR); +} + +void Tunnel::Fail(const std::string &msg) { + LOGE("[%{public}s] %{public}s", LOG_TAG, msg.c_str()); + if (finished_.exchange(true)) return; + if (onState_) onState_("error", msg.c_str()); +} + +void Tunnel::Run() { + if (onState_) onState_("connecting", ""); + + // Reserve the loopback source port first so the USB DDK server can lock + // its listener to exactly this tunnel before any bytes flow. + const int localFd = ::socket(AF_INET, SOCK_STREAM, 0); + if (localFd < 0) { + Fail("local socket creation failed"); + return; + } + localFd_.store(localFd); + { + sockaddr_in bindAddr{}; + bindAddr.sin_family = AF_INET; + bindAddr.sin_addr.s_addr = inet_addr(config_.localHost.c_str()); + bindAddr.sin_port = 0; + if (::bind(localFd, reinterpret_cast(&bindAddr), sizeof(bindAddr)) < 0) { + Fail("local bind failed"); + ::close(localFd); + localFd_.store(-1); + return; + } + sockaddr_in bound{}; + socklen_t boundLen = sizeof(bound); + ::getsockname(localFd, reinterpret_cast(&bound), &boundLen); + if (config_.onLocalBound) { + config_.onLocalBound(ntohs(bound.sin_port)); + } + } + + const int remoteFd = ConnectTcp(config_.host, config_.port, kConnectTimeoutSec); + if (remoteFd < 0) { + Fail("Sunshine connect failed: " + config_.host + ":" + std::to_string(config_.port)); + ::close(localFd); + localFd_.store(-1); + return; + } + remoteFd_.store(remoteFd); + + // Bound read timeout for the whole startup phase (TLS + JSON + attach). + timeval tv{ kHandshakeTimeoutSec, 0 }; + setsockopt(remoteFd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + + SSL_CTX *ctx = SSL_CTX_new(TLS_client_method()); + if (ctx == nullptr) { + Fail("SSL_CTX_new failed"); + ::close(localFd); + ::close(remoteFd); + localFd_.store(-1); + remoteFd_.store(-1); + return; + } + sslCtx_ = ctx; + + // Paired client identity. + { + BIO *certBio = BIO_new_mem_buf(config_.clientCertPem.data(), + static_cast(config_.clientCertPem.size())); + BIO *keyBio = BIO_new_mem_buf(config_.clientKeyPem.data(), + static_cast(config_.clientKeyPem.size())); + X509 *cert = certBio ? PEM_read_bio_X509(certBio, nullptr, nullptr, nullptr) : nullptr; + EVP_PKEY *key = keyBio ? PEM_read_bio_PrivateKey(keyBio, nullptr, nullptr, nullptr) : nullptr; + if (cert == nullptr || key == nullptr || + SSL_CTX_use_certificate(ctx, cert) != 1 || + SSL_CTX_use_PrivateKey(ctx, key) != 1) { + if (cert) X509_free(cert); + if (key) EVP_PKEY_free(key); + if (certBio) BIO_free(certBio); + if (keyBio) BIO_free(keyBio); + Fail("failed to load paired client certificate/key"); + ::close(localFd); + ::close(remoteFd); + localFd_.store(-1); + remoteFd_.store(-1); + return; + } + X509_free(cert); + EVP_PKEY_free(key); + BIO_free(certBio); + BIO_free(keyBio); + } + + // Chain/hostname verification cannot express "trust exactly this paired + // self-signed certificate" (mirrors nvhttp and the qt/Android tunnels): + // verify manually by comparing DER below. + SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, nullptr); + + X509 *pinned = nullptr; + { + BIO *pinBio = BIO_new_mem_buf(config_.serverCertPem.data(), + static_cast(config_.serverCertPem.size())); + pinned = pinBio ? PEM_read_bio_X509(pinBio, nullptr, nullptr, nullptr) : nullptr; + if (pinBio) BIO_free(pinBio); + } + if (pinned == nullptr) { + Fail("failed to load pinned server certificate"); + ::close(localFd); + ::close(remoteFd); + localFd_.store(-1); + remoteFd_.store(-1); + return; + } + + SSL *ssl = SSL_new(ctx); + SSL_set_fd(ssl, remoteFd); + + bool ok = true; + if (SSL_connect(ssl) != 1) { + unsigned long err = ERR_get_error(); + char buf[256]; + ERR_error_string_n(err, buf, sizeof(buf)); + Fail(std::string("TLS handshake failed: ") + buf); + ok = false; + } else { + X509 *peer = SSL_get1_peer_certificate(ssl); + if (peer == nullptr) { + Fail("TLS peer did not present a certificate"); + ok = false; + } else if (X509_cmp(peer, pinned) != 0) { + X509_free(peer); + Fail("server certificate does not match the pairing pin"); + ok = false; + } else { + X509_free(peer); + } + } + X509_free(pinned); + if (!ok) { + SSL_free(ssl); + ::close(localFd); + ::close(remoteFd); + localFd_.store(-1); + remoteFd_.store(-1); + return; + } + LOGI("[%{public}s] TLS established with %{public}s:%{public}u", LOG_TAG, + config_.host.c_str(), config_.port); + + // One-line JSON handshake, then opaque USB/IP bytes. + { + const std::string line = "{\"op\":\"forward\",\"token\":\"" + config_.sessionToken + + "\",\"busid\":\"" + config_.localBusId + "\"}\n"; + if (SSL_write(ssl, line.data(), static_cast(line.size())) <= 0) { + Fail("failed to send the USB tunnel handshake"); + SSL_free(ssl); + ::close(localFd); + ::close(remoteFd); + localFd_.store(-1); + remoteFd_.store(-1); + return; + } + } + { + std::string buf; + buf.reserve(128); + char c; + while (buf.size() < kMaxHandshakeBytes) { + const int n = SSL_read(ssl, &c, 1); + if (n <= 0) { + Fail("tunnel closed during handshake"); + SSL_free(ssl); + ::close(localFd); + ::close(remoteFd); + localFd_.store(-1); + remoteFd_.store(-1); + return; + } + if (c == '\n') break; + buf += c; + } + // The only success reply is {"op":"ready"}; anything else carries a + // reason. A substring scan is enough for this fixed contract. + if (buf.find("\"op\":\"ready\"") == std::string::npos && + buf.find("\"op\": \"ready\"") == std::string::npos) { + Fail("Sunshine refused: " + extractReason(buf)); + SSL_free(ssl); + ::close(localFd); + ::close(remoteFd); + localFd_.store(-1); + remoteFd_.store(-1); + return; + } + } + + // Attach the reserved local connection and start pumping. + { + sockaddr_in localAddr{}; + localAddr.sin_family = AF_INET; + localAddr.sin_addr.s_addr = inet_addr(config_.localHost.c_str()); + localAddr.sin_port = htons(config_.localPort); + if (::connect(localFd, reinterpret_cast(&localAddr), sizeof(localAddr)) < 0) { + Fail("local USB/IP server connect failed"); + SSL_free(ssl); + ::close(localFd); + ::close(remoteFd); + localFd_.store(-1); + remoteFd_.store(-1); + return; + } + } + + // Handshake phase over: pump pacing is poll-driven from here on. + tv.tv_sec = 0; + setsockopt(remoteFd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + + if (onState_) onState_("ready", ""); + LOGI("[%{public}s] forwarding busid %{public}s", LOG_TAG, config_.localBusId.c_str()); + + Pump(localFd, ssl); + + SSL_shutdown(ssl); + SSL_free(ssl); + ::close(localFd); + ::close(remoteFd); + localFd_.store(-1); + remoteFd_.store(-1); + if (!finished_.exchange(true) && onState_) { + onState_("closed", ""); + } +} + +void Tunnel::Pump(int localFd, void *sslPtr) { + SSL *ssl = static_cast(sslPtr); + const int remoteFd = SSL_get_fd(ssl); + uint8_t buf[kIoBufferSize]; + + while (running_.load() && !finished_.load()) { + // SSL may have buffered a record beyond the last poll. + if (SSL_pending(ssl) > 0) { + const int n = SSL_read(ssl, buf, sizeof(buf)); + if (n <= 0) return; + if (!WriteAllPlain(localFd, buf, static_cast(n))) return; + continue; + } + pollfd fds[2]{}; + fds[0].fd = localFd; + fds[0].events = POLLIN; + fds[1].fd = remoteFd; + fds[1].events = POLLIN; + const int rc = ::poll(fds, 2, 500); + if (rc < 0) return; + if (rc == 0) continue; // timeout: loop re-checks running_ + + if (fds[0].revents & (POLLIN | POLLHUP)) { + const ssize_t n = ::read(localFd, buf, sizeof(buf)); + if (n <= 0) return; + size_t off = 0; + while (off < static_cast(n)) { + const int written = SSL_write(ssl, buf + off, static_cast(n - off)); + if (written <= 0) return; + off += static_cast(written); + } + } + if (fds[1].revents & (POLLIN | POLLHUP)) { + const int n = SSL_read(ssl, buf, sizeof(buf)); + if (n <= 0) return; + if (!WriteAllPlain(localFd, buf, static_cast(n))) return; + } + } +} + +} // namespace usbip diff --git a/nativelib/src/main/cpp/usbip_tunnel.h b/nativelib/src/main/cpp/usbip_tunnel.h new file mode 100644 index 0000000..1a64a92 --- /dev/null +++ b/nativelib/src/main/cpp/usbip_tunnel.h @@ -0,0 +1,85 @@ +/* + * Moonlight for HarmonyOS + * Copyright (C) 2025 Moonlight/AlkaidLab + * + * usbip_tunnel - Reverse USB/IP tunnel client (HarmonyOS native port). + * + * Port of moonlight-qt's UsbForwarding::Tunnel. Connects a local USB/IP + * server (usbip::Server on 127.0.0.1) to Sunshine's TLS endpoint with a + * one-line JSON handshake, then pumps opaque bytes in both directions. + * + * Wire contract (docs/remote-usb-reverse-tunnel.md, Sunshine + * reverse_tunnel_service.cpp): + * C → S: {"op":"forward","token":"","busid":"1-2"}\n + * S → C: {"op":"ready"}\n or {"op":"error","reason":"..."}\n + * (then raw USB/IP bytes after "ready") + * + * TLS mirrors the qt/Android clients: chain/hostname verification stays off, + * and the peer certificate must DER-match the certificate pinned at pairing + * time. The paired client certificate authenticates us to Sunshine. + */ + +#ifndef USBIP_TUNNEL_H +#define USBIP_TUNNEL_H + +#include +#include +#include +#include +#include + +namespace usbip { + +struct TunnelConfig { + std::string host; // Sunshine address + uint16_t port = 47996; // from /api/v1/usb-forwarding capability + std::string sessionToken; // one-shot token from the same endpoint + std::string clientCertPem; // paired client certificate + std::string clientKeyPem; // paired client private key + std::string serverCertPem; // pin target (paired server certificate) + std::string localHost = "127.0.0.1"; + uint16_t localPort = 3240; // usbip::Server port + std::string localBusId; // busid to forward (e.g. "1-1") + + // Called on the tunnel thread with the bound loopback source port right + // after the local socket is bound, before either connect. The default + // USB DDK server uses it to lock the listener to this tunnel. + std::function onLocalBound; +}; + +class Tunnel { +public: + using StateCallback = std::function; + + explicit Tunnel(TunnelConfig config); + ~Tunnel(); + + // Connects both sockets asynchronously; state is reported through the + // callback on the tunnel thread: "connecting", "ready", "closed", + // "error". After "ready" the pump moves USB/IP bytes verbatim. + void Start(StateCallback onState); + void Stop() noexcept; // thread-safe; unblocks the pump via shutdown() + + bool IsValid() const; + +private: + void Run(); // thread body + void Pump(int localFd, void *ssl); + void Fail(const std::string &msg); + void WakeSockets() noexcept; + + TunnelConfig config_; + StateCallback onState_; + std::atomic running_{false}; + std::atomic finished_{false}; + std::thread thread_; + // fds are published as soon as they exist so Stop() can shutdown() them + // and unblock a connect/handshake/pump in progress. -1 = not yet open. + std::atomic localFd_{-1}; + std::atomic remoteFd_{-1}; + void *sslCtx_ = nullptr; // SSL_CTX*, void* to avoid header leak +}; + +} // namespace usbip + +#endif // USBIP_TUNNEL_H From b81258e14d48ec521453559154ab07c44812c3e4 Mon Sep 17 00:00:00 2001 From: qiin2333 <414382190@qq.com> Date: Fri, 11 Sep 2026 16:10:49 +0800 Subject: [PATCH 5/8] fix(usbip): address review findings on PR #132 - usbip_server: RET_SUBMIT/RET_UNLINK headers were appended past the pre-sized 48-byte buffer instead of written at protocol offsets, so every URB reply was malformed on the wire. Add storeU32/storeI32 and write fields at their fixed offsets. - usbip_server: publish the accepted client fd and shutdown() it in Stop(), so a handler parked on a half-PDU read from a stalled peer cannot wedge the accept-thread join. - usbip_napi: guard tunnel state/message with a dedicated mutex (the tunnel thread writes them while JS threads query; g_mutex may be held across the joining Stop); free the tsfn event when a nonblocking enqueue fails, as ownership stays with the caller. - usbip_tunnel: resolve the Sunshine endpoint with getaddrinfo so IPv6 literals and hostnames connect instead of silently targeting 255.255.255.255; publish fds as -1 before close() at every cleanup site so a concurrent WakeSockets() cannot shutdown a recycled fd. - UsbForwardingService: reclaim leftover resources at begin() entry - a superseded flow skips teardown on its generation guard. - CHANGELOG: qualify the passthrough feature wording (no isochronous endpoints yet, hardware validation in progress). Co-Authored-By: Claude Fable 5 --- .../usbdriver/UsbForwardingService.ets | 7 ++ entry/src/main/resources/rawfile/CHANGELOG.md | 2 +- nativelib/src/main/cpp/usbip_napi.cpp | 55 ++++++--- nativelib/src/main/cpp/usbip_server.cpp | 44 +++++-- nativelib/src/main/cpp/usbip_server.h | 3 + nativelib/src/main/cpp/usbip_tunnel.cpp | 112 ++++++++++-------- 6 files changed, 143 insertions(+), 80 deletions(-) diff --git a/entry/src/main/ets/service/usbdriver/UsbForwardingService.ets b/entry/src/main/ets/service/usbdriver/UsbForwardingService.ets index cb391f8..253ffb1 100644 --- a/entry/src/main/ets/service/usbdriver/UsbForwardingService.ets +++ b/entry/src/main/ets/service/usbdriver/UsbForwardingService.ets @@ -207,6 +207,13 @@ export class UsbForwardingService { return; } + // 入口统一回收上一轮资源:上一轮 begin 可能仍停在 await(权限弹窗、 + // capability 查询),其代数守卫返回时不会自行 teardown,这里补齐, + // 避免旧 USBDevicePipe/native server 泄漏或被新一轮覆盖。 + if (this.active || this.activeDeviceKey !== '' || this.heldPipe !== null) { + this.teardown(false); + } + // 挑选转发目标:第一个未被本地驱动占用的已知手柄 const activeKeys = UsbDriverService.getInstance().getActiveDeviceKeys(); let target: usbManager.USBDevice | null = null; diff --git a/entry/src/main/resources/rawfile/CHANGELOG.md b/entry/src/main/resources/rawfile/CHANGELOG.md index 63f5155..ae87386 100644 --- a/entry/src/main/resources/rawfile/CHANGELOG.md +++ b/entry/src/main/resources/rawfile/CHANGELOG.md @@ -29,7 +29,7 @@ OTG 手柄 USB 直通主机(USB/IP 反向隧道,实验性) ### 新增 -- 通过 OTG 连接的手柄可经 USB/IP 反向隧道直通 Sunshine 主机,作为原生 USB 设备工作:陀螺仪、触控板等完整特性由主机直接驱动,输入不经网络手柄协议转译。 +- 通过 OTG 连接的手柄可经 USB/IP 反向隧道直通 Sunshine 主机,作为原生 USB 设备工作:陀螺仪、触控板等主机驱动支持的特性可直接使用,输入不经网络手柄协议转译(同步端点如音频通道暂不支持;真机链路验证进行中)。 - 主机侧通过配对证书 + 一次性令牌建立 TLS 反向隧道(Sunshine `usbip-forwarding` 能力),与 moonlight-qt / moonlight-android 的 USB 转发同源契约。 - 本地 USB/IP 服务仅接受隧道专用回环端口连接,其他本机进程无法访问被导出的设备。 - 设置中新增「USB 直通主机(实验)」开关;被转发的手柄自动从本地 USB 驱动中排除,转发失败时回退为本地驱动处理。 diff --git a/nativelib/src/main/cpp/usbip_napi.cpp b/nativelib/src/main/cpp/usbip_napi.cpp index e239289..dfca2af 100644 --- a/nativelib/src/main/cpp/usbip_napi.cpp +++ b/nativelib/src/main/cpp/usbip_napi.cpp @@ -49,6 +49,10 @@ std::shared_ptr g_ddk; std::unique_ptr g_server; std::unique_ptr g_tunnel; napi_threadsafe_function g_tunnelTsfn = nullptr; +// Written from the tunnel thread, read from JS threads querying status. +// Guarded by its own lock: g_mutex may be held while joining the tunnel +// thread (Stop*), so taking it from the callback would deadlock. +std::mutex g_stateMutex; TunnelState g_tunnelState = TunnelState::kIdle; std::string g_tunnelMessage; @@ -144,8 +148,11 @@ napi_value StopServer(napi_env env, napi_callback_info info) { napi_release_threadsafe_function(g_tunnelTsfn, napi_tsfn_release); g_tunnelTsfn = nullptr; } - g_tunnelState = TunnelState::kIdle; - g_tunnelMessage.clear(); + { + std::lock_guard stateLock(g_stateMutex); + g_tunnelState = TunnelState::kIdle; + g_tunnelMessage.clear(); + } if (g_server) { g_server->Stop(); g_server.reset(); @@ -295,29 +302,36 @@ napi_value StartTunnel(napi_env env, napi_callback_info info) { Server *server = g_server.get(); config.onLocalBound = [server](uint16_t port) { server->AuthorizePort(port); }; - g_tunnelState = TunnelState::kConnecting; - g_tunnelMessage.clear(); + { + std::lock_guard stateLock(g_stateMutex); + g_tunnelState = TunnelState::kConnecting; + g_tunnelMessage.clear(); + } g_tunnel = std::make_unique(std::move(config)); g_tunnel->Start([](const char *state, const char *message) { // Runs on the tunnel thread: update the pollable snapshot and // forward to JS. - if (strcmp(state, "connecting") == 0) g_tunnelState = TunnelState::kConnecting; - else if (strcmp(state, "ready") == 0) g_tunnelState = TunnelState::kReady; - else if (strcmp(state, "closed") == 0) g_tunnelState = TunnelState::kClosed; - else g_tunnelState = TunnelState::kError; - g_tunnelMessage = message ? message : ""; + std::string messageCopy = message ? message : ""; + { + std::lock_guard stateLock(g_stateMutex); + if (strcmp(state, "connecting") == 0) g_tunnelState = TunnelState::kConnecting; + else if (strcmp(state, "ready") == 0) g_tunnelState = TunnelState::kReady; + else if (strcmp(state, "closed") == 0) g_tunnelState = TunnelState::kClosed; + else g_tunnelState = TunnelState::kError; + g_tunnelMessage = messageCopy; + } auto *event = new (std::nothrow) TunnelEvent{}; if (event == nullptr) return; strncpy(event->state, state, sizeof(event->state) - 1); - const size_t len = g_tunnelMessage.size(); - event->message = static_cast(malloc(len + 1)); + event->message = static_cast(malloc(messageCopy.size() + 1)); if (event->message != nullptr) { - memcpy(event->message, g_tunnelMessage.c_str(), len + 1); + memcpy(event->message, messageCopy.c_str(), messageCopy.size() + 1); } - if (g_tunnelTsfn != nullptr) { - napi_call_threadsafe_function(g_tunnelTsfn, event, napi_tsfn_nonblocking); - } else { + // On queue-full/closing the call fails and ownership of the data + // stays with us. + if (g_tunnelTsfn == nullptr || + napi_call_threadsafe_function(g_tunnelTsfn, event, napi_tsfn_nonblocking) != napi_ok) { if (event->message) free(event->message); delete event; } @@ -332,15 +346,18 @@ napi_value StopTunnel(napi_env env, napi_callback_info info) { napi_create_object(env, &result); std::lock_guard lock(g_mutex); if (g_tunnel) { - g_tunnel->Stop(); + g_tunnel->Stop(); // joins the tunnel thread, which may report state g_tunnel.reset(); } if (g_tunnelTsfn != nullptr) { napi_release_threadsafe_function(g_tunnelTsfn, napi_tsfn_release); g_tunnelTsfn = nullptr; } - g_tunnelState = TunnelState::kIdle; - g_tunnelMessage.clear(); + { + std::lock_guard stateLock(g_stateMutex); + g_tunnelState = TunnelState::kIdle; + g_tunnelMessage.clear(); + } setFieldInt(env, result, "code", 0); return result; } @@ -348,7 +365,7 @@ napi_value StopTunnel(napi_env env, napi_callback_info info) { napi_value TunnelStateQuery(napi_env env, napi_callback_info info) { napi_value result; napi_create_object(env, &result); - std::lock_guard lock(g_mutex); + std::lock_guard stateLock(g_stateMutex); setFieldStr(env, result, "state", tunnelStateName(g_tunnelState)); setFieldStr(env, result, "message", g_tunnelMessage.c_str()); return result; diff --git a/nativelib/src/main/cpp/usbip_server.cpp b/nativelib/src/main/cpp/usbip_server.cpp index 8fa301d..68b8bc1 100644 --- a/nativelib/src/main/cpp/usbip_server.cpp +++ b/nativelib/src/main/cpp/usbip_server.cpp @@ -112,6 +112,18 @@ int32_t readI32(const uint8_t *p) { return static_cast(readU32(p)); } +// Big-endian store at a fixed offset. PDU reply headers are pre-sized +// buffers; appendU32 would push fields past the end of the header. +void storeU32(uint8_t *p, int off, uint32_t v) { + p[off] = (v >> 24) & 0xFF; + p[off + 1] = (v >> 16) & 0xFF; + p[off + 2] = (v >> 8) & 0xFF; + p[off + 3] = v & 0xFF; +} +void storeI32(uint8_t *p, int off, int32_t v) { + storeU32(p, off, static_cast(v)); +} + bool writeAll(int fd, const uint8_t *data, size_t len) { size_t off = 0; while (off < len) { @@ -327,6 +339,12 @@ void Server::Stop() noexcept { ::close(listenFd_); listenFd_ = -1; } + // Unblock the handler thread: it may be parked in a blocking read on + // the accepted connection (half a PDU from a stalled remote peer). + const int client = clientFd_.exchange(-1); + if (client >= 0) { + ::shutdown(client, SHUT_RDWR); + } if (acceptThread_.joinable()) { acceptThread_.join(); } @@ -453,7 +471,11 @@ void Server::AcceptLoop() { } authorizedPort_.store(-1); // one shot: this connection is the tunnel + // Publish so Stop() can shutdown() the socket out from under a + // blocking read; retract before close to avoid fd-reuse races. + clientFd_.store(fd); HandleConnection(fd); + clientFd_.store(-1); ::close(fd); } } @@ -629,14 +651,16 @@ void Server::PumpUrbLoop(int clientFd, const DeviceInfo &device, const auto sendRetSubmit = [&](uint32_t seqnum, int32_t status, int32_t actual, const uint8_t *data = nullptr) { + // usbip_header: command[0] seqnum[4] ... status[20] actual_length[24] + // start_frame[28] number_of_packets[32] error_count[36]; rest zero. std::vector ret(kPduHeaderSize + (actual > 0 ? actual : 0), 0); - appendU32(ret, kRetSubmit); - appendU32(ret, seqnum); - appendI32(ret, status); // [20] - appendI32(ret, actual); // [24] actual_length - appendI32(ret, 0); // [28] start_frame - appendI32(ret, -1); // [32] number_of_packets (not isochronous) - appendI32(ret, 0); // [36] error_count + storeU32(ret.data(), kCmdOffset, kRetSubmit); + storeU32(ret.data(), kSeqnumOffset, seqnum); + storeI32(ret.data(), kFlagsOrUnlinkTargetOffset, status); + storeI32(ret.data(), kLenOrActualOffset, actual); + storeI32(ret.data(), kStartFrameOffset, 0); + storeI32(ret.data(), kNumPacketsOrErrorOffset, -1); // not isochronous + storeI32(ret.data(), kErrorCountOffset, 0); if (actual > 0 && data != nullptr) { std::memcpy(ret.data() + kPduHeaderSize, data, static_cast(actual)); } @@ -644,9 +668,9 @@ void Server::PumpUrbLoop(int clientFd, const DeviceInfo &device, }; const auto sendRetUnlink = [&](uint32_t seqnum, int32_t status) { std::vector ret(kPduHeaderSize, 0); - appendU32(ret, kRetUnlink); - appendU32(ret, seqnum); - appendI32(ret, status); // [20] + storeU32(ret.data(), kCmdOffset, kRetUnlink); + storeU32(ret.data(), kSeqnumOffset, seqnum); + storeI32(ret.data(), kFlagsOrUnlinkTargetOffset, status); return writeVec(clientFd, ret); }; diff --git a/nativelib/src/main/cpp/usbip_server.h b/nativelib/src/main/cpp/usbip_server.h index c9fab74..4e1da37 100644 --- a/nativelib/src/main/cpp/usbip_server.h +++ b/nativelib/src/main/cpp/usbip_server.h @@ -152,6 +152,9 @@ class Server { int boundPort_ = 0; std::atomic running_{false}; std::atomic authorizedPort_{-1}; + // Accepted connection owned by the accept thread; published so Stop() + // can shutdown() it and unblock any read the handler is stuck in. + std::atomic clientFd_{-1}; std::thread acceptThread_; mutable std::mutex devicesMutex_; std::vector devices_; diff --git a/nativelib/src/main/cpp/usbip_tunnel.cpp b/nativelib/src/main/cpp/usbip_tunnel.cpp index 28e1c20..808543b 100644 --- a/nativelib/src/main/cpp/usbip_tunnel.cpp +++ b/nativelib/src/main/cpp/usbip_tunnel.cpp @@ -8,11 +8,13 @@ #include "usbip_tunnel.h" #include +#include #include #include #include #include +#include #include #include #include @@ -58,42 +60,52 @@ bool ReadAllPlain(int fd, uint8_t *buf, size_t len) { return true; } +// Resolves hostnames and IPv4/IPv6 literals, then connects with a timeout. +// Tries each resolved address until one connects. int ConnectTcp(const std::string &host, uint16_t port, int timeoutSec) { - const int fd = ::socket(AF_INET, SOCK_STREAM, 0); - if (fd < 0) return -1; - - // Non-blocking connect with timeout. - const int flags = fcntl(fd, F_GETFL, 0); - fcntl(fd, F_SETFL, flags | O_NONBLOCK); - - sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_port = htons(port); - addr.sin_addr.s_addr = inet_addr(host.c_str()); - - const int rc = ::connect(fd, reinterpret_cast(&addr), sizeof(addr)); - if (rc < 0 && errno != EINPROGRESS) { - ::close(fd); + char portStr[8] = {}; + snprintf(portStr, sizeof(portStr), "%u", static_cast(port)); + + addrinfo hints{}; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + addrinfo *list = nullptr; + if (getaddrinfo(host.c_str(), portStr, &hints, &list) != 0 || list == nullptr) { return -1; } - if (rc != 0) { - pollfd pfd{fd, POLLOUT, 0}; - if (::poll(&pfd, 1, timeoutSec * 1000) <= 0) { - ::close(fd); - return -1; + + int fd = -1; + for (addrinfo *ai = list; ai != nullptr; ai = ai->ai_next) { + fd = ::socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (fd < 0) continue; + + const int flags = fcntl(fd, F_GETFL, 0); + fcntl(fd, F_SETFL, flags | O_NONBLOCK); + + const int rc = ::connect(fd, ai->ai_addr, ai->ai_addrlen); + bool connected = false; + if (rc == 0) { + connected = true; + } else if (errno == EINPROGRESS) { + pollfd pfd{fd, POLLOUT, 0}; + if (::poll(&pfd, 1, timeoutSec * 1000) > 0) { + int err = 0; + socklen_t errLen = sizeof(err); + getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errLen); + connected = (err == 0); + } } - int err = 0; - socklen_t errLen = sizeof(err); - getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errLen); - if (err != 0) { - ::close(fd); - return -1; + + if (connected) { + fcntl(fd, F_SETFL, flags); // back to blocking + int nodelay = 1; + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)); + break; } + ::close(fd); + fd = -1; } - fcntl(fd, F_SETFL, flags); // back to blocking - - int nodelay = 1; - setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)); + freeaddrinfo(list); return fd; } @@ -181,8 +193,8 @@ void Tunnel::Run() { bindAddr.sin_port = 0; if (::bind(localFd, reinterpret_cast(&bindAddr), sizeof(bindAddr)) < 0) { Fail("local bind failed"); - ::close(localFd); localFd_.store(-1); + ::close(localFd); return; } sockaddr_in bound{}; @@ -196,8 +208,8 @@ void Tunnel::Run() { const int remoteFd = ConnectTcp(config_.host, config_.port, kConnectTimeoutSec); if (remoteFd < 0) { Fail("Sunshine connect failed: " + config_.host + ":" + std::to_string(config_.port)); - ::close(localFd); localFd_.store(-1); + ::close(localFd); return; } remoteFd_.store(remoteFd); @@ -209,10 +221,10 @@ void Tunnel::Run() { SSL_CTX *ctx = SSL_CTX_new(TLS_client_method()); if (ctx == nullptr) { Fail("SSL_CTX_new failed"); - ::close(localFd); - ::close(remoteFd); localFd_.store(-1); remoteFd_.store(-1); + ::close(localFd); + ::close(remoteFd); return; } sslCtx_ = ctx; @@ -233,10 +245,10 @@ void Tunnel::Run() { if (certBio) BIO_free(certBio); if (keyBio) BIO_free(keyBio); Fail("failed to load paired client certificate/key"); - ::close(localFd); - ::close(remoteFd); localFd_.store(-1); remoteFd_.store(-1); + ::close(localFd); + ::close(remoteFd); return; } X509_free(cert); @@ -259,10 +271,10 @@ void Tunnel::Run() { } if (pinned == nullptr) { Fail("failed to load pinned server certificate"); - ::close(localFd); - ::close(remoteFd); localFd_.store(-1); remoteFd_.store(-1); + ::close(localFd); + ::close(remoteFd); return; } @@ -292,10 +304,10 @@ void Tunnel::Run() { X509_free(pinned); if (!ok) { SSL_free(ssl); - ::close(localFd); - ::close(remoteFd); localFd_.store(-1); remoteFd_.store(-1); + ::close(localFd); + ::close(remoteFd); return; } LOGI("[%{public}s] TLS established with %{public}s:%{public}u", LOG_TAG, @@ -308,10 +320,10 @@ void Tunnel::Run() { if (SSL_write(ssl, line.data(), static_cast(line.size())) <= 0) { Fail("failed to send the USB tunnel handshake"); SSL_free(ssl); - ::close(localFd); - ::close(remoteFd); localFd_.store(-1); remoteFd_.store(-1); + ::close(localFd); + ::close(remoteFd); return; } } @@ -324,10 +336,10 @@ void Tunnel::Run() { if (n <= 0) { Fail("tunnel closed during handshake"); SSL_free(ssl); - ::close(localFd); - ::close(remoteFd); localFd_.store(-1); remoteFd_.store(-1); + ::close(localFd); + ::close(remoteFd); return; } if (c == '\n') break; @@ -339,10 +351,10 @@ void Tunnel::Run() { buf.find("\"op\": \"ready\"") == std::string::npos) { Fail("Sunshine refused: " + extractReason(buf)); SSL_free(ssl); - ::close(localFd); - ::close(remoteFd); localFd_.store(-1); remoteFd_.store(-1); + ::close(localFd); + ::close(remoteFd); return; } } @@ -356,10 +368,10 @@ void Tunnel::Run() { if (::connect(localFd, reinterpret_cast(&localAddr), sizeof(localAddr)) < 0) { Fail("local USB/IP server connect failed"); SSL_free(ssl); - ::close(localFd); - ::close(remoteFd); localFd_.store(-1); remoteFd_.store(-1); + ::close(localFd); + ::close(remoteFd); return; } } @@ -375,10 +387,10 @@ void Tunnel::Run() { SSL_shutdown(ssl); SSL_free(ssl); - ::close(localFd); - ::close(remoteFd); localFd_.store(-1); remoteFd_.store(-1); + ::close(localFd); + ::close(remoteFd); if (!finished_.exchange(true) && onState_) { onState_("closed", ""); } From b4910a7897329999763de769c7abf16c89697fb7 Mon Sep 17 00:00:00 2001 From: qiin2333 <414382190@qq.com> Date: Fri, 11 Sep 2026 16:53:16 +0800 Subject: [PATCH 6/8] fix(usbip): define PDU header offsets used by RET_SUBMIT/RET_UNLINK The review fix referenced kStartFrameOffset/kNumPacketsOrErrorOffset/ kErrorCountOffset, which were dropped from the constants block during the server rewrite - CI (and any real compile) failed with undeclared identifiers. Restore them (offset 36 is interval in CMD_SUBMIT and error_count in RET_SUBMIT) and drop the now-unused appendI32. The earlier local verification missed this: the build ran with the nativelib dependency resolving through a stale oh_modules junction into the main checkout (a different branch's tree) and empty submodules, so the worktree native build never compiled this file. This build compiles usbip_server.cpp for both ABIs from the worktree's own sources. Co-Authored-By: Claude Fable 5 --- nativelib/src/main/cpp/usbip_server.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/nativelib/src/main/cpp/usbip_server.cpp b/nativelib/src/main/cpp/usbip_server.cpp index 68b8bc1..3210c54 100644 --- a/nativelib/src/main/cpp/usbip_server.cpp +++ b/nativelib/src/main/cpp/usbip_server.cpp @@ -77,14 +77,19 @@ constexpr int kConfigValueOffset = 0x135; constexpr int kNumConfigsOffset = 0x136; constexpr int kNumInterfacesOffset = 0x137; -// 48-byte PDU header offsets +// 48-byte PDU header offsets. The payload is a union per direction: +// [20] transfer_flags / status / unlink target, [24] transfer_buffer_length +// / actual_length, [36] interval / error_count. constexpr int kCmdOffset = 0; constexpr int kSeqnumOffset = 4; constexpr int kDirectionOffset = 12; constexpr int kEpOffset = 16; constexpr int kFlagsOrUnlinkTargetOffset = 20; constexpr int kLenOrActualOffset = 24; +constexpr int kStartFrameOffset = 28; +constexpr int kNumPacketsOrErrorOffset = 32; constexpr int kIntervalOffset = 36; +constexpr int kErrorCountOffset = 36; constexpr int kSetupOffset = 40; // setup[8] void appendU16(std::vector &out, uint16_t v) { @@ -97,9 +102,6 @@ void appendU32(std::vector &out, uint32_t v) { out.push_back((v >> 8) & 0xFF); out.push_back(v & 0xFF); } -void appendI32(std::vector &out, int32_t v) { - appendU32(out, static_cast(v)); -} uint16_t readU16(const uint8_t *p) { return (uint16_t(p[0]) << 8) | uint16_t(p[1]); From 8846acad9beacb956da515882e69aa8d40df3ac5 Mon Sep 17 00:00:00 2001 From: qiin2333 <414382190@qq.com> Date: Fri, 11 Sep 2026 17:12:38 +0800 Subject: [PATCH 7/8] fix(usbip): address round-2 review findings on PR #132 - runHeadInUrb: a MSG_PEEK returning fewer than 48 bytes is a partial PDU from TCP segmentation, not a dead connection. Only EOF and hard socket errors close the drive; partial headers hand off to the main loop's blocking readAll, and EAGAIN continues the device wait. - Stop/AcceptLoop: publish the accepted client under a lifecycle mutex paired with Stop()'s teardown, so a connection accepted just before teardown can never be published after Stop's shutdown sweep (which would leave join() waiting for the session to end on its own). - LoadDdk: require the control-transfer pair and DestroyDeviceMemMap, and reject a torn config-descriptor pair (exactly one present); previously SendControlRead/WriteRequest were dereferenced unchecked and FreeConfigDescriptor was called unguarded. - sendRetSubmit: allocate payload bytes only when data is provided; the header still reports actual_length (OUT replies carry no payload). Co-Authored-By: Claude Fable 5 --- nativelib/src/main/cpp/usbip_server.cpp | 99 ++++++++++++++++++------- nativelib/src/main/cpp/usbip_server.h | 3 + 2 files changed, 74 insertions(+), 28 deletions(-) diff --git a/nativelib/src/main/cpp/usbip_server.cpp b/nativelib/src/main/cpp/usbip_server.cpp index 3210c54..a8a22e7 100644 --- a/nativelib/src/main/cpp/usbip_server.cpp +++ b/nativelib/src/main/cpp/usbip_server.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -232,9 +233,17 @@ std::shared_ptr LoadDdk() { api->DestroyDeviceMemMap = reinterpret_cast(load("OH_Usb_DestroyDeviceMemMap")); + // The server drives control transfers and walks config descriptors, so + // those entry points are load-bearing. GetConfigDescriptor and + // FreeConfigDescriptor form a pair: a torn table (exactly one present) + // would crash at first use, reject it outright. + const bool configPairOk = + (api->GetConfigDescriptor == nullptr) == (api->FreeConfigDescriptor == nullptr); if (api->GetDeviceDescriptor == nullptr || api->ClaimInterface == nullptr || - api->SendPipeRequest == nullptr || api->CreateDeviceMemMap == nullptr) { - LOGE("[%{public}s] USB DDK core functions missing", LOG_TAG); + api->SendPipeRequest == nullptr || api->CreateDeviceMemMap == nullptr || + api->DestroyDeviceMemMap == nullptr || api->SendControlReadRequest == nullptr || + api->SendControlWriteRequest == nullptr || !configPairOk) { + LOGE("[%{public}s] USB DDK function table incomplete", LOG_TAG); dlclose(api->handle); return nullptr; } @@ -336,17 +345,25 @@ int Server::Start(std::string *error) { void Server::Stop() noexcept { if (!running_.exchange(false)) return; - if (listenFd_ >= 0) { - ::shutdown(listenFd_, SHUT_RDWR); - ::close(listenFd_); - listenFd_ = -1; - } - // Unblock the handler thread: it may be parked in a blocking read on - // the accepted connection (half a PDU from a stalled remote peer). - const int client = clientFd_.exchange(-1); - if (client >= 0) { - ::shutdown(client, SHUT_RDWR); + { + // Lifecycle lock: whoever holds it either tears the listener down or + // publishes the accepted client. If AcceptLoop is past the gate it + // has already published clientFd_, so the exchange below sees it. + std::lock_guard lock(lifecycleMutex_); + if (listenFd_ >= 0) { + ::shutdown(listenFd_, SHUT_RDWR); + ::close(listenFd_); + listenFd_ = -1; + } + // Unblock the handler thread: it may be parked in a blocking read on + // the accepted connection (half a PDU from a stalled remote peer). + const int client = clientFd_.exchange(-1); + if (client >= 0) { + ::shutdown(client, SHUT_RDWR); + } } + // join outside the lock: the accept thread takes it only around the + // gate/publish points, never around accept() or HandleConnection(). if (acceptThread_.joinable()) { acceptThread_.join(); } @@ -471,11 +488,22 @@ void Server::AcceptLoop() { ::close(fd); continue; } - authorizedPort_.store(-1); // one shot: this connection is the tunnel - // Publish so Stop() can shutdown() the socket out from under a - // blocking read; retract before close to avoid fd-reuse races. - clientFd_.store(fd); + // Publish under the lifecycle lock, paired with Stop(): if teardown + // already began, close the fd here instead of handing it a stale + // clientFd_ that Stop's exchange would miss (join would then hang + // until the connection ends on its own). + { + std::lock_guard lock(lifecycleMutex_); + if (!running_.load()) { + ::close(fd); + break; + } + authorizedPort_.store(-1); // one shot: this connection is the tunnel + // Publish so Stop() can shutdown() the socket out from under a + // blocking read; retract before close to avoid fd-reuse races. + clientFd_.store(fd); + } HandleConnection(fd); clientFd_.store(-1); ::close(fd); @@ -655,7 +683,9 @@ void Server::PumpUrbLoop(int clientFd, const DeviceInfo &device, const uint8_t *data = nullptr) { // usbip_header: command[0] seqnum[4] ... status[20] actual_length[24] // start_frame[28] number_of_packets[32] error_count[36]; rest zero. - std::vector ret(kPduHeaderSize + (actual > 0 ? actual : 0), 0); + // The header reports actual_length regardless; payload bytes exist + // only when data is provided (OUT replies carry no payload). + std::vector ret(kPduHeaderSize + (actual > 0 && data != nullptr ? actual : 0), 0); storeU32(ret.data(), kCmdOffset, kRetSubmit); storeU32(ret.data(), kSeqnumOffset, seqnum); storeI32(ret.data(), kFlagsOrUnlinkTargetOffset, status); @@ -716,19 +746,32 @@ void Server::PumpUrbLoop(int clientFd, const DeviceInfo &device, // yield to whatever else the host queued. if (waitReadable(clientFd, 0)) { uint8_t peek[kPduHeaderSize]; - if (::recv(clientFd, peek, sizeof(peek), MSG_PEEK) < - static_cast(sizeof(peek))) { - return Drive::Closed; + const ssize_t peeked = ::recv(clientFd, peek, sizeof(peek), MSG_PEEK); + if (peeked == 0) { + return Drive::Closed; // orderly EOF + } + if (peeked == static_cast(sizeof(peek))) { + if (readU32(peek + kCmdOffset) == kCmdUnlink && + readU32(peek + kFlagsOrUnlinkTargetOffset) == p.seqnum) { + uint8_t pdu[kPduHeaderSize]; + if (!readAll(clientFd, pdu, sizeof(pdu))) return Drive::Closed; + return sendRetUnlink(readU32(pdu + kSeqnumOffset), 0) + ? Drive::Completed + : Drive::Closed; + } + return Drive::Suspended; // some other full PDU queued + } + if (peeked > 0) { + // TCP delivered only part of the header; a live peer + // is mid-PDU. Hand off to the main loop's blocking + // readAll, which completes it (and is unbounded only + // until Stop() shuts the socket down). + return Drive::Suspended; } - if (readU32(peek + kCmdOffset) == kCmdUnlink && - readU32(peek + kFlagsOrUnlinkTargetOffset) == p.seqnum) { - uint8_t pdu[kPduHeaderSize]; - if (!readAll(clientFd, pdu, sizeof(pdu))) return Drive::Closed; - return sendRetUnlink(readU32(pdu + kSeqnumOffset), 0) - ? Drive::Completed - : Drive::Closed; + if (errno == EAGAIN || errno == EWOULDBLOCK) { + continue; // spurious readability: keep waiting } - return Drive::Suspended; + return Drive::Closed; // hard socket error } continue; // no news from the host: keep waiting on the device } diff --git a/nativelib/src/main/cpp/usbip_server.h b/nativelib/src/main/cpp/usbip_server.h index 4e1da37..aeb3e5d 100644 --- a/nativelib/src/main/cpp/usbip_server.h +++ b/nativelib/src/main/cpp/usbip_server.h @@ -155,6 +155,9 @@ class Server { // Accepted connection owned by the accept thread; published so Stop() // can shutdown() it and unblock any read the handler is stuck in. std::atomic clientFd_{-1}; + // Serializes Stop()'s teardown against the accept thread's gate/publish + // of a new client, so Stop never joins a connection it failed to wake. + std::mutex lifecycleMutex_; std::thread acceptThread_; mutable std::mutex devicesMutex_; std::vector devices_; From 25c0ab35b008023482ff4b8af5e64ea5135143c3 Mon Sep 17 00:00:00 2001 From: qiin2333 <414382190@qq.com> Date: Fri, 11 Sep 2026 17:24:53 +0800 Subject: [PATCH 8/8] fix(usbip): RET_UNLINK reports -ECONNRESET for canceled pending URBs Both CMD_UNLINK paths (the timeout-slice peek in runHeadInUrb and processPdu) replied status 0 when the target IN URB was still queued, which the host reads as a successfully completed URB. A URB canceled before RET_SUBMIT carries -ECONNRESET, matching what a real USB stack reports for unlinked URBs; URBs no longer pending keep the ENOENT reply. Co-Authored-By: Claude Fable 5 --- nativelib/src/main/cpp/usbip_server.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/nativelib/src/main/cpp/usbip_server.cpp b/nativelib/src/main/cpp/usbip_server.cpp index a8a22e7..5ed142d 100644 --- a/nativelib/src/main/cpp/usbip_server.cpp +++ b/nativelib/src/main/cpp/usbip_server.cpp @@ -57,6 +57,7 @@ constexpr uint32_t kRetUnlink = 0x00000004; constexpr int32_t kUsbErrIo = -5; // -EIO constexpr int32_t kUsbErrNoent = -2; // -ENOENT +constexpr int32_t kUsbErrConnreset = -104; // -ECONNRESET (URB canceled before completion) constexpr int32_t kUsbErrOverflow = -75; // -EOVERFLOW constexpr int32_t kUsbErrTimedout = -110;// -ETIMEDOUT @@ -755,7 +756,10 @@ void Server::PumpUrbLoop(int clientFd, const DeviceInfo &device, readU32(peek + kFlagsOrUnlinkTargetOffset) == p.seqnum) { uint8_t pdu[kPduHeaderSize]; if (!readAll(clientFd, pdu, sizeof(pdu))) return Drive::Closed; - return sendRetUnlink(readU32(pdu + kSeqnumOffset), 0) + // Head URB is still pending (no RET_SUBMIT yet): + // report it as canceled, like a real unlinked URB. + return sendRetUnlink(readU32(pdu + kSeqnumOffset), + kUsbErrConnreset) ? Drive::Completed : Drive::Closed; } @@ -904,9 +908,10 @@ void Server::PumpUrbLoop(int clientFd, const DeviceInfo &device, const uint32_t target = readU32(pdu + kFlagsOrUnlinkTargetOffset); for (auto it = pendingIn.begin(); it != pendingIn.end(); ++it) { if (it->seqnum == target) { - // Safe to touch here: we only run between DDK slices. + // Still pending (no RET_SUBMIT sent): report it as + // canceled, mirroring a real unlinked URB's status. pendingIn.erase(it); - return sendRetUnlink(seqnum, 0); + return sendRetUnlink(seqnum, kUsbErrConnreset); } } // Already completed: ENOENT, mirroring the kernel stub.