Files
punktfunk/crates/punktfunk-host/vendor/usbip-sim/src/usbip_protocol.rs
T
enricobuehler 580b1ea7a7
apple / screenshots (push) Has been cancelled
android / android (push) Has been cancelled
apple / swift (push) Has been cancelled
audit / cargo-audit (push) Has been cancelled
ci / web (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / bench (push) Has been cancelled
ci / rust (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
release / apple (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
windows-host / package (push) Has been cancelled
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Has been cancelled
windows / build (aarch64-pc-windows-msvc) (push) Has been cancelled
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
feat(host/steam): shippable usbip/vhci_hcd virtual Deck + client leave-shortcuts
Steam Deck pass-through (design/steam-deck-passthrough-plan.md), code-complete +
all CI checks green on Linux + adversarially reviewed; on-glass validation pending:

- usbip/`vhci_hcd` virtual Deck transport (inject/linux/steam_usbip.rs) for
  non-SteamOS hosts (Bazzite/generic) — presents a real interface-2 USB Deck so
  Steam Input promotes it. In-process vhci attach (loopback OP_REQ_IMPORT handshake
  → sysfs attach) with a bounded `usbip`-CLI fallback; detach on drop.
- Backed by a vendored, libusb-free trim of the `usbip` crate
  (crates/punktfunk-host/vendor/usbip-sim, MIT + NOTICE; host/cdc/hid + rusb/nusb
  removed; interrupt-IN paced by bInterval).
- Selection ladder raw_gadget (SteamOS fast-path) → usbip (universal) → UHID,
  with PUNKTFUNK_STEAM_USBIP / PUNKTFUNK_USBIP_ATTACH knobs.
- Shared Deck descriptors + the 0x83/0xAE feature contract + a Steam-accepted
  serial consolidated into steam_proto.rs; the raw_gadget backend reuses them.
- Linux client leave-shortcuts: Ctrl+Alt+Shift+D + holding the escape chord
  (L1+R1+Start+Select) >=1.5s end the session (short press still exits
  fullscreen); the chord state resets across sessions.

Also bundles in-progress work already staged in the tree:
- host(kwin): xdg-output logical-geometry mapping so the KWin fake_input backend
  places absolute coordinates correctly under display scaling.
- docs: design/README index entries + design/controller-only-mode.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 19:17:37 +00:00

499 lines
17 KiB
Rust

//! USB/IP protocol structs
//!
//! This module contains declarations of all structs used in the USB/IP protocol,
//! as well as functions to serialize and deserialize them to/from byte arrays,
//! and functions to send and receive them over a socket.
//!
//! They are based on the [Linux kernel documentation](https://docs.kernel.org/usb/usbip_protocol.html).
use log::trace;
use std::io::Result;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::UsbDevice;
/// USB/IP protocol version
///
/// This is currently the only supported version of USB/IP
/// for this library.
pub const USBIP_VERSION: u16 = 0x0111;
/// Command code: Retrieve the list of exported USB devices
pub const OP_REQ_DEVLIST: u16 = 0x8005;
/// Command code: import a remote USB device
pub const OP_REQ_IMPORT: u16 = 0x8003;
/// Reply code: The list of exported USB devices
pub const OP_REP_DEVLIST: u16 = 0x0005;
/// Reply code: Reply to import
pub const OP_REP_IMPORT: u16 = 0x0003;
/// Command code: Submit an URB
pub const USBIP_CMD_SUBMIT: u16 = 0x0001;
/// Command code: Unlink an URB
pub const USBIP_CMD_UNLINK: u16 = 0x0002;
/// Reply code: Reply for submitting an URB
pub const USBIP_RET_SUBMIT: u16 = 0x0003;
/// Reply code: Reply for URB unlink
pub const USBIP_RET_UNLINK: u16 = 0x0004;
/// USB/IP direction
///
/// NOTE: Must not be confused with rusb::Direction,
/// which has the opposite enum values. This is only for
/// internal use in the USB/IP protocol.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Direction {
Out = 0,
In = 1,
}
/// Common header for all context sensitive packets
///
/// All commands/responses which rely on a device being attached
/// to a client use this header.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct UsbIpHeaderBasic {
pub command: u32,
pub seqnum: u32,
pub devid: u32,
pub direction: u32,
pub ep: u32,
}
impl UsbIpHeaderBasic {
/// Converts a byte array into a [UsbIpHeaderBasic].
pub fn from_bytes(bytes: &[u8; 20]) -> Self {
let result = UsbIpHeaderBasic {
command: u32::from_be_bytes(bytes[0..4].try_into().unwrap()),
seqnum: u32::from_be_bytes(bytes[4..8].try_into().unwrap()),
devid: u32::from_be_bytes(bytes[8..12].try_into().unwrap()),
direction: u32::from_be_bytes(bytes[12..16].try_into().unwrap()),
ep: u32::from_be_bytes(bytes[16..20].try_into().unwrap()),
};
// The direction should be 0 or 1
debug_assert!(result.direction & 1 == result.direction);
result
}
/// Converts the [UsbIpHeaderBasic] into a byte array.
pub fn to_bytes(&self) -> [u8; 20] {
let mut result = [0u8; 20];
result[0..4].copy_from_slice(&self.command.to_be_bytes());
result[4..8].copy_from_slice(&self.seqnum.to_be_bytes());
result[8..12].copy_from_slice(&self.devid.to_be_bytes());
result[12..16].copy_from_slice(&self.direction.to_be_bytes());
result[16..20].copy_from_slice(&self.ep.to_be_bytes());
result
}
pub(crate) async fn read_from_socket_with_command<T: AsyncReadExt + Unpin>(
socket: &mut T,
command: u16,
) -> Result<Self> {
let seqnum = socket.read_u32().await?;
let devid = socket.read_u32().await?;
let direction = socket.read_u32().await?;
// The direction should be 0 or 1
debug_assert!(direction & 1 == direction);
let ep = socket.read_u32().await?;
Ok(UsbIpHeaderBasic {
command: command.into(),
seqnum,
devid,
direction,
ep,
})
}
}
/// Client side commands from the Virtual Host Controller
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum UsbIpCommand {
OpReqDevlist {
status: u32,
},
OpReqImport {
status: u32,
busid: [u8; 32],
},
UsbIpCmdSubmit {
header: UsbIpHeaderBasic,
transfer_flags: u32,
transfer_buffer_length: u32,
start_frame: u32,
number_of_packets: u32,
interval: u32,
setup: [u8; 8],
data: Vec<u8>,
iso_packet_descriptor: Vec<u8>,
},
UsbIpCmdUnlink {
header: UsbIpHeaderBasic,
unlink_seqnum: u32,
},
}
impl UsbIpCommand {
/// Constructs a [UsbIpCommand] from a socket
///
/// This will consume a variable amount of bytes from the socket.
/// It might fail if the bytes does not follow the USB/IP protocol properly.
pub async fn read_from_socket<T: AsyncReadExt + Unpin>(socket: &mut T) -> Result<UsbIpCommand> {
let version: u16 = socket.read_u16().await?;
if version != 0 && version != USBIP_VERSION {
return Err(std::io::Error::other(format!(
"Unknown version: {version:#04X}"
)));
}
let command: u16 = socket.read_u16().await?;
trace!(
"Received command: {:#04X} ({}), parsing...",
command,
match command {
OP_REQ_DEVLIST => "OP_REQ_DEVLIST",
OP_REQ_IMPORT => "OP_REQ_IMPORT",
USBIP_CMD_SUBMIT => "USBIP_CMD_SUBMIT",
USBIP_CMD_UNLINK => "USBIP_CMD_UNLINK",
_ => "Unknown",
}
);
match command {
OP_REQ_DEVLIST => {
let status = socket.read_u32().await?;
debug_assert!(status == 0);
Ok(UsbIpCommand::OpReqDevlist { status })
}
OP_REQ_IMPORT => {
let status = socket.read_u32().await?;
debug_assert!(status == 0);
let mut busid = [0; 32];
socket.read_exact(&mut busid).await?;
Ok(UsbIpCommand::OpReqImport { status, busid })
}
USBIP_CMD_SUBMIT => {
let header =
UsbIpHeaderBasic::read_from_socket_with_command(socket, USBIP_CMD_SUBMIT)
.await?;
let transfer_flags = socket.read_u32().await?;
let transfer_buffer_length = socket.read_u32().await?;
let start_frame = socket.read_u32().await?;
let number_of_packets = socket.read_u32().await?;
let interval = socket.read_u32().await?;
let mut setup = [0; 8];
socket.read_exact(&mut setup).await?;
let data = if header.direction == Direction::In as u32 {
vec![]
} else {
let mut data = vec![0; transfer_buffer_length as usize];
socket.read_exact(&mut data).await?;
data
};
// The kernel docs specifies that this should be set to 0xFFFFFFFF for all
// non-ISO packets, however the actual implementation resorts to 0x00000000
// https://stackoverflow.com/questions/76899798/usb-ip-what-is-the-size-of-the-iso-packet-descriptor
let iso_packet_descriptor =
if number_of_packets != 0 && number_of_packets != 0xFFFFFFFF {
let mut result = vec![0; 16 * number_of_packets as usize];
socket.read_exact(&mut result).await?;
result
} else {
vec![]
};
Ok(UsbIpCommand::UsbIpCmdSubmit {
header,
transfer_flags,
transfer_buffer_length,
start_frame,
number_of_packets,
interval,
setup,
data,
iso_packet_descriptor,
})
}
USBIP_CMD_UNLINK => {
let header =
UsbIpHeaderBasic::read_from_socket_with_command(socket, USBIP_CMD_UNLINK)
.await?;
let unlink_seqnum = socket.read_u32().await?;
let mut _padding = [0; 24];
socket.read_exact(&mut _padding).await?;
Ok(UsbIpCommand::UsbIpCmdUnlink {
header,
unlink_seqnum,
})
}
_ => Err(std::io::Error::other(format!(
"Unknown command: {command:#04X}"
))),
}
}
/// Converts the [UsbIpCommand] into a byte vector
pub fn to_bytes(&self) -> Vec<u8> {
match *self {
UsbIpCommand::OpReqDevlist { status } => {
let mut result = Vec::with_capacity(8);
result.extend_from_slice(&USBIP_VERSION.to_be_bytes());
result.extend_from_slice(&OP_REQ_DEVLIST.to_be_bytes());
result.extend_from_slice(&status.to_be_bytes());
result
}
UsbIpCommand::OpReqImport { status, busid } => {
let mut result = Vec::with_capacity(40);
result.extend_from_slice(&USBIP_VERSION.to_be_bytes());
result.extend_from_slice(&OP_REQ_IMPORT.to_be_bytes());
result.extend_from_slice(&status.to_be_bytes());
result.extend_from_slice(&busid);
result
}
UsbIpCommand::UsbIpCmdSubmit {
ref header,
transfer_flags,
transfer_buffer_length,
start_frame,
number_of_packets,
interval,
setup,
ref data,
ref iso_packet_descriptor,
} => {
debug_assert!(
header.direction != Direction::Out as u32
|| transfer_buffer_length == data.len() as u32
);
let mut result = Vec::with_capacity(48 + data.len() + iso_packet_descriptor.len());
result.extend_from_slice(&header.to_bytes());
result.extend_from_slice(&transfer_flags.to_be_bytes());
result.extend_from_slice(&transfer_buffer_length.to_be_bytes());
result.extend_from_slice(&start_frame.to_be_bytes());
result.extend_from_slice(&number_of_packets.to_be_bytes());
result.extend_from_slice(&interval.to_be_bytes());
result.extend_from_slice(&setup);
result.extend_from_slice(data);
result.extend_from_slice(iso_packet_descriptor);
result
}
UsbIpCommand::UsbIpCmdUnlink {
ref header,
unlink_seqnum,
} => {
let mut result = Vec::with_capacity(48);
result.extend_from_slice(&header.to_bytes());
result.extend_from_slice(&unlink_seqnum.to_be_bytes());
result.extend_from_slice(&[0; 24]);
result
}
}
}
}
/// Server side responses from the USB Host
#[derive(Clone)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum UsbIpResponse {
OpRepDevlist {
status: u32,
device_count: u32,
devices: Vec<UsbDevice>,
},
OpRepImport {
status: u32,
device: Option<UsbDevice>,
},
UsbIpRetSubmit {
header: UsbIpHeaderBasic,
status: u32,
actual_length: u32,
start_frame: u32,
number_of_packets: u32,
error_count: u32,
transfer_buffer: Vec<u8>,
iso_packet_descriptor: Vec<u8>,
},
UsbIpRetUnlink {
header: UsbIpHeaderBasic,
status: u32,
},
}
impl UsbIpResponse {
/// Converts the [UsbIpResponse] into a byte vector
pub fn to_bytes(&self) -> Vec<u8> {
match *self {
Self::OpRepDevlist {
status,
device_count,
ref devices,
} => {
let mut result = Vec::with_capacity(
12 + devices.len() * 312
+ devices
.iter()
.map(|d| d.interfaces.len() * 4)
.sum::<usize>(),
);
result.extend_from_slice(&USBIP_VERSION.to_be_bytes());
result.extend_from_slice(&OP_REP_DEVLIST.to_be_bytes());
result.extend_from_slice(&status.to_be_bytes());
result.extend_from_slice(&device_count.to_be_bytes());
for dev in devices {
result.extend_from_slice(&dev.to_bytes_with_interfaces());
}
result
}
Self::OpRepImport { status, ref device } => {
let mut result = Vec::with_capacity(320);
result.extend_from_slice(&USBIP_VERSION.to_be_bytes());
result.extend_from_slice(&OP_REP_IMPORT.to_be_bytes());
result.extend_from_slice(&status.to_be_bytes());
if let Some(device) = device {
result.extend_from_slice(&device.to_bytes());
}
result
}
Self::UsbIpRetSubmit {
ref header,
status,
actual_length,
start_frame,
number_of_packets,
error_count,
ref transfer_buffer,
ref iso_packet_descriptor,
} => {
let mut result =
Vec::with_capacity(48 + transfer_buffer.len() + iso_packet_descriptor.len());
debug_assert!(header.command == USBIP_RET_SUBMIT.into());
debug_assert!(if header.direction == Direction::In as u32 {
actual_length == transfer_buffer.len() as u32
} else {
actual_length == 0
});
result.extend_from_slice(&header.to_bytes());
result.extend_from_slice(&status.to_be_bytes());
result.extend_from_slice(&actual_length.to_be_bytes());
result.extend_from_slice(&start_frame.to_be_bytes());
result.extend_from_slice(&number_of_packets.to_be_bytes());
result.extend_from_slice(&error_count.to_be_bytes());
result.extend_from_slice(&[0; 8]);
result.extend_from_slice(transfer_buffer);
result.extend_from_slice(iso_packet_descriptor);
result
}
Self::UsbIpRetUnlink { ref header, status } => {
let mut result = Vec::with_capacity(48);
debug_assert!(header.command == USBIP_RET_UNLINK.into());
result.extend_from_slice(&header.to_bytes());
result.extend_from_slice(&status.to_be_bytes());
result.extend_from_slice(&[0; 24]);
result
}
}
}
pub async fn write_to_socket<T: AsyncWriteExt + Unpin>(&self, socket: &mut T) -> Result<()> {
socket.write_all(&self.to_bytes()).await
}
/// Constructs a OP_REP_DEVLIST response
pub fn op_rep_devlist(devices: &[UsbDevice]) -> Self {
Self::OpRepDevlist {
status: 0,
device_count: devices.len() as u32,
devices: devices.to_vec(),
}
}
/// Constructs a successful OP_REP_IMPORT response
pub fn op_rep_import_success(device: &UsbDevice) -> Self {
Self::OpRepImport {
status: 0,
device: Some(device.clone()),
}
}
/// Constructs a failed OP_REP_IMPORT response
pub fn op_rep_import_fail() -> Self {
Self::OpRepImport {
status: 1,
device: None,
}
}
/// Constructs a successful OP_REP_IMPORT response
pub fn usbip_ret_submit_success(
header: &UsbIpHeaderBasic,
start_frame: u32,
number_of_packets: u32,
transfer_buffer: Vec<u8>,
iso_packet_descriptor: Vec<u8>,
) -> Self {
Self::UsbIpRetSubmit {
header: header.clone(),
status: 0,
actual_length: transfer_buffer.len() as u32,
start_frame,
number_of_packets,
error_count: 0,
transfer_buffer,
iso_packet_descriptor,
}
}
/// Constructs a failed OP_REP_IMPORT response
pub fn usbip_ret_submit_fail(header: &UsbIpHeaderBasic) -> Self {
Self::UsbIpRetSubmit {
header: header.clone(),
status: 1,
actual_length: 0,
start_frame: 0,
number_of_packets: 0,
error_count: 0,
transfer_buffer: vec![],
iso_packet_descriptor: vec![],
}
}
/// Constructs a successful OP_REP_IMPORT response
pub fn usbip_ret_unlink_success(header: &UsbIpHeaderBasic) -> Self {
Self::UsbIpRetUnlink {
header: header.clone(),
status: 0,
}
}
/// Constructs a failed OP_REP_IMPORT response.
pub fn usbip_ret_unlink_fail(header: &UsbIpHeaderBasic) -> Self {
Self::UsbIpRetUnlink {
header: header.clone(),
status: 1,
}
}
}
// (In-crate test module removed in the vendored copy — see NOTICE.)