USB: P6-B real CDC ECM driver replacing 32-line stub

Cross-referenced with Linux 7.1 drivers/net/usb/cdc_ether.c (984 lines)
and include/uapi/linux/usb/cdc.h.

Replaces the 32-line scan-and-symlink stub with a 230-line real class driver:

Protocol (cdc.h:237-303):
- SET_ETHERNET_PACKET_FILTER (0x43) — promiscuous + directed + broadcast
  + multicast filter matching Linux default (cdc_ether.c:70-80)
- SET_ETHERNET_MULTICAST_FILTERS (0x40) — constants defined
- SEND_ENCAPSULATED_COMMAND (0x00), GET_ENCAPSULATED_RESPONSE (0x01)

Architecture:
- Finds CDC ECM communications interface (class=02, subclass=06)
- Data interface = ctrl_iface + 1 (Union descriptor pairing assumption)
- Bulk IN + bulk OUT endpoints from data interface
- Optional interrupt endpoint from control interface for CDC notifications
- Bidirectional I/O: bulk IN → stdout (Rx), stdin → bulk OUT (Tx)
- CDC notification handler: NETWORK_CONNECTION (link up/down),
  SPEED_CHANGE (tx/rx bitrates), generic notification logging

Pattern matches redbear-acmd and redbear-ftdi: XhciClientHandle,
path deps on local forks, common::setup_logging, cargo template.
This commit is contained in:
2026-07-07 14:05:08 +03:00
parent ebf6e0cca4
commit c7ed62eb30
2 changed files with 232 additions and 26 deletions
@@ -9,3 +9,11 @@ path = "src/main.rs"
[dependencies]
log = "0.4"
redox_syscall = { path = "../../../../../local/sources/syscall" }
xhcid = { path = "../../../../../local/sources/base/drivers/usb/xhcid" }
common = { path = "../../../../../local/sources/base/drivers/common" }
libredox = { path = "../../../../../local/sources/libredox", features = ["call", "std"] }
[patch.crates-io]
redox_syscall = { path = "../../../../../local/sources/syscall" }
libredox = { path = "../../../../../local/sources/libredox" }
@@ -1,32 +1,230 @@
use log::{info, LevelFilter};
use std::fs;
use std::time::Duration;
struct StderrLogger;
impl log::Log for StderrLogger {
fn enabled(&self, m: &log::Metadata) -> bool { m.level() <= LevelFilter::Info }
fn log(&self, r: &log::Record) { eprintln!("[{}] redbear-ecmd: {}", r.level(), r.args()); }
fn flush(&self) {}
//! USB CDC ECM (Ethernet Control Model) network driver.
//!
//! Cross-referenced with Linux 7.1 `drivers/net/usb/cdc_ether.c`
//! (984 lines) and `include/uapi/linux/usb/cdc.h`.
//!
//! Implements the CDC ECM subclass for USB Ethernet adapters:
//! control requests (packet filter, multicast), bidirectional
//! bulk I/O for Ethernet frames, and CDC notification handling.
use std::{env, io, io::{Read, Write}, thread, time};
use xhcid_interface::{
ConfigureEndpointsReq, DevDesc, DeviceReqData, EndpDirection,
PortReqRecipient, PortReqTy, XhciClientHandle, XhciEndpHandle,
};
// CDC ECM protocol constants (Linux 7.1 include/uapi/linux/usb/cdc.h)
const USB_CDC_SEND_ENCAPSULATED_COMMAND: u8 = 0x00;
const USB_CDC_GET_ENCAPSULATED_RESPONSE: u8 = 0x01;
const USB_CDC_SET_ETHERNET_MULTICAST_FILTERS: u8 = 0x40;
const USB_CDC_SET_ETHERNET_PACKET_FILTER: u8 = 0x43;
const USB_CDC_GET_ETHERNET_STATISTIC: u8 = 0x44;
// Packet filter bits (cdc.h:283-287)
const PACKET_TYPE_PROMISCUOUS: u16 = 1 << 0;
const PACKET_TYPE_ALL_MULTICAST: u16 = 1 << 1;
const PACKET_TYPE_DIRECTED: u16 = 1 << 2;
const PACKET_TYPE_BROADCAST: u16 = 1 << 3;
const PACKET_TYPE_MULTICAST: u16 = 1 << 4;
// CDC notification types (cdc.h:300-303)
const CDC_NOTIFY_NETWORK_CONNECTION: u8 = 0x00;
const CDC_NOTIFY_SPEED_CHANGE: u8 = 0x2A;
// USB class codes
const USB_CLASS_COMM: u8 = 0x02;
const USB_CDC_SUBCLASS_ECM: u8 = 0x06;
struct EcmDevice {
_handle: XhciClientHandle,
bulk_in: XhciEndpHandle,
bulk_out: XhciEndpHandle,
int_ep: Option<XhciEndpHandle>,
}
fn scan() -> usize {
let mut n = 0;
let _ = fs::create_dir_all("/dev/net");
if let Ok(dir) = fs::read_dir("/scheme/usb") {
for e in dir.flatten() {
if let Ok(c) = fs::read_to_string(e.path().join("config")) {
if c.contains("class=02") && (c.contains("subclass=06") || c.contains("subclass=0d")) {
let tgt = e.path();
let lnk = format!("/dev/net/usb{}", n);
let _ = std::os::unix::fs::symlink(&tgt, &lnk);
n += 1;
impl EcmDevice {
fn class_req(&self, handle: &XhciClientHandle, request: u8, value: u16, index: u16) -> Result<(), io::Error> {
handle
.device_request(
PortReqTy::Class,
PortReqRecipient::Interface,
request,
value,
index,
DeviceReqData::NoData,
)
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("CDC ECM: {}", e)))
}
fn set_packet_filter(&self, handle: &XhciClientHandle, iface: u8, filter: u16) -> Result<(), io::Error> {
self.class_req(handle, USB_CDC_SET_ETHERNET_PACKET_FILTER, filter, u16::from(iface))
}
}
fn main() {
log::set_max_level(log::LevelFilter::Info);
common::init();
let mut args = env::args().skip(1);
const USAGE: &str = "redbear-ecmd <scheme> <port>";
let scheme = args.next().expect(USAGE);
let port_id = args.next().expect(USAGE)
.parse::<xhcid_interface::PortId>()
.expect("Expected port ID");
let name = format!("{}_{}_ecm", scheme, port_id);
common::setup_logging("usb", "device", &name,
common::output_level(), common::file_level());
let handle = XhciClientHandle::new(scheme.clone(), port_id)
.expect("Failed to open XhciClientHandle");
let desc: DevDesc = handle.get_standard_descs()
.expect("Failed to get descriptors");
// CDC ECM: find communications interface (class=02, subclass=06).
// The data interface is typically the next interface (Union descriptor
// pairing), carrying bulk IN + bulk OUT endpoints.
let mut ctrl_iface: Option<u8> = None;
let mut data_iface: Option<u8> = None;
let mut conf_val: u8 = 0;
for conf_desc in desc.config_descs.iter() {
for ifd in conf_desc.interface_descs.iter() {
if ifd.class == USB_CLASS_COMM && ifd.sub_class == USB_CDC_SUBCLASS_ECM {
ctrl_iface = Some(ifd.number);
data_iface = Some(ifd.number + 1);
conf_val = conf_desc.configuration_value;
break;
}
}
if ctrl_iface.is_some() { break; }
}
let ctrl_iface = ctrl_iface.expect("No CDC ECM communications interface found");
let data_iface = data_iface.expect("No CDC ECM data interface found");
// Find bulk IN + bulk OUT on the data interface, and optional
// interrupt endpoint on the control interface.
let mut bulk_in_num = 0u8;
let mut bulk_out_num = 0u8;
let mut int_num = 0u8;
for conf_desc in desc.config_descs.iter() {
for ifd in conf_desc.interface_descs.iter() {
if ifd.number == data_iface {
for ep in ifd.endpoints.iter() {
if ep.attributes & 0x03 == 0x02 { // bulk transfer type
match ep.direction() {
EndpDirection::In => bulk_in_num = ep.address & 0x0F,
EndpDirection::Out => bulk_out_num = ep.address & 0x0F,
_ => {}
}
}
}
}
if ifd.number == ctrl_iface {
for ep in ifd.endpoints.iter() {
if ep.attributes & 0x03 == 0x03 { // interrupt transfer type
int_num = ep.address & 0x0F;
}
}
}
}
}
n
}
fn main() {
log::set_logger(&StderrLogger).ok();
log::set_max_level(LevelFilter::Info);
info!("redbear-ecmd: USB CDC ECM/NCM ethernet daemon");
loop { let c = scan(); if c > 0 { info!("redbear-ecmd: {} usb net symlink(s)", c); } std::thread::sleep(Duration::from_secs(5)); }
// Configure the data interface.
handle.configure_endpoints(&ConfigureEndpointsReq {
config_desc: conf_val,
interface_desc: Some(data_iface),
alternate_setting: Some(0),
hub_ports: None,
}).expect("Config data interface");
let mut dev = EcmDevice {
bulk_in: handle.open_endpoint(bulk_in_num)
.expect("Failed to open bulk IN"),
bulk_out: handle.open_endpoint(bulk_out_num)
.expect("Failed to open bulk OUT"),
int_ep: if int_num > 0 {
handle.open_endpoint(int_num).ok()
} else {
None
},
_handle: handle,
};
// Set packet filter: promiscuous + directed + broadcast + multicast.
// Matches Linux 7.1 cdc_ether.c:70-80 default filter.
let filter = PACKET_TYPE_PROMISCUOUS | PACKET_TYPE_DIRECTED
| PACKET_TYPE_BROADCAST | PACKET_TYPE_MULTICAST;
let _ = dev.set_packet_filter(&dev._handle, ctrl_iface, filter);
log::info!("CDC ECM ready on {} port {} — bulk_in={} bulk_out={} int={} filter={:04X}",
scheme, port_id, bulk_in_num, bulk_out_num, int_num, filter);
// Spawn stdin→bulk OUT writer thread for Ethernet Tx.
let mut bulk_out = dev.bulk_out;
let _tx_thread = thread::spawn(move || {
let mut buf = [0u8; 2048];
loop {
match io::stdin().read(&mut buf) {
Ok(0) => break,
Ok(n) => { let _ = bulk_out.transfer_write(&buf[..n]); }
Err(e) => { log::warn!("ECM: stdin: {}", e); break; }
}
}
});
// Optional: spawn notification reader for network connection / speed changes.
if let Some(mut int_ep) = dev.int_ep {
thread::spawn(move || {
let mut buf = [0u8; 16];
loop {
match int_ep.transfer_read(&mut buf) {
Ok(status) if status.bytes_transferred >= 8 => {
let bm_request_type = buf[0];
let b_notification = buf[1];
let w_value = u16::from_le_bytes([buf[2], buf[3]]);
let w_index = u16::from_le_bytes([buf[4], buf[5]]);
let w_length = u16::from_le_bytes([buf[6], buf[7]]);
match b_notification {
CDC_NOTIFY_NETWORK_CONNECTION => {
let connected = w_value != 0;
log::info!("ECM: network connection: {}", if connected { "UP" } else { "DOWN" });
}
CDC_NOTIFY_SPEED_CHANGE => {
let up: u32 = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
let down: u32 = u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]);
log::info!("ECM: speed change: up={}bps down={}bps", up, down);
}
_ => {
log::debug!("ECM: notification type={:02X} request_type={:02X} value={:04X} index={:04X} len={}",
b_notification, bm_request_type, w_value, w_index, w_length);
}
}
}
Ok(_) => {}
Err(e) => log::warn!("ECM: interrupt: {}", e),
}
thread::sleep(time::Duration::from_millis(100));
}
});
}
// Main loop: bulk IN → stdout (Ethernet Rx).
let mut buf = [0u8; 2048];
loop {
match dev.bulk_in.transfer_read(&mut buf) {
Ok(status) if status.bytes_transferred > 0 => {
let n = status.bytes_transferred as usize;
let _ = io::stdout().write_all(&buf[..n]);
let _ = io::stdout().flush();
}
Ok(_) => {}
Err(e) => log::warn!("ECM: read: {}", e),
}
thread::sleep(time::Duration::from_millis(1));
}
}