Driverless USB Proxy over SCSI and CD-ROM Emulation
A historical note on turning a constrained CD-ROM path into a client/server redirect route
Driverless USB Proxy over SCSI and CD-ROM Emulation
A historical note on turning a constrained CD-ROM path into a client/server redirect route
This is a historical engineering note from a project I worked on around 2009–2012.

The project is easy to summarize as “USB-SCSI communication,” but that is not the part that made it valuable.
The real problem was product-shaped: the USB device did not have many endpoints to spare, but it still needed both a driverless first-run experience and a live communication path.
The device needed to appear to a Windows or macOS host as a virtual CD-ROM, so the user could access the companion application without installing a custom driver first. At the same time, the device also needed a host-device communication path for control and data traffic. Above that communication path, the product also needed a proxy-style redirect layer that could bridge USB traffic with client/server socket traffic.
The obvious solution would have been a USB composite device: Mass Storage for the virtual CD-ROM, plus CDC, HID, vendor bulk, or a network function for communication.
But the endpoint budget was tight, and the product requirement was driverless. So the question became:
Can the CD-ROM path itself carry the communication, and can that route become stable enough to support a proxy?
The answer was yes, but only if I treated USB Mass Storage as what it really is: a SCSI transport.
The Product Constraint
The device was an embedded Linux USB device, not a USB host.

It had to provide a familiar first-run experience:
- plug the device into a PC or Mac
- the host sees a virtual CD-ROM
- the user opens the companion application from that virtual disc
- the application then talks back to the embedded device
That last part was the hard one.
If I added another USB communication function, I would spend more endpoints. If I asked users to install a custom kernel driver, I would damage the product experience. If I depended on a platform-specific driver stack, I would have to maintain separate host-side behavior forever.
So I looked at the layer already present.
Under the CD-ROM icon, the host was not talking to “a folder.” It was talking to a USB Mass Storage device. Under Mass Storage, the host was sending SCSI commands such as READ(10) and WRITE(10) over bulk transfers.
That existing path already had:
- a host-visible device
- read/write transfer phases
- built-in OS class drivers
- Windows and macOS user-space access paths
- bulk IN/OUT endpoints already allocated
The communication path was already there. The trick was to use it without breaking the normal CD-ROM behavior.
The Architecture
At a high level, the data path looked like this:

Windows / macOS host
|
v
User application
|
v
SCSI pass-through API
- Windows: IOCTL_SCSI_PASS_THROUGH_DIRECT
- macOS: IOKit + SCSITaskLib
|
v
USB Mass Storage transport
|
v
Embedded Linux gadget / mass-storage layer
|
v
Private device-side buffer and user-space node
|
v
Embedded application logic
From the user’s point of view, it was still a virtual CD-ROM.
From the host application’s point of view, it was a library with ordinary calls:
pwUsbHostOpen(...)
pwUsbHostRead(...)
pwUsbHostWrite(...)
pwUsbHostClose(...)
From the embedded side, it was a device node and a stream of data that could be read and written by the application layer.
The hard part was making these three views agree.
The Host Side: Two OSes, One Transport Idea
The public host API was intentionally small:
pwUsbHostDeviceFind(...)
pwUsbHostUsbFind(...)
pwUsbHostDeviceMonitorRegister(...)
pwUsbHostOpen(...)
pwUsbHostRead(...)
pwUsbHostWrite(...)
pwUsbHostClose(...)
That was the product-facing shape. Internally, Windows and macOS required very different plumbing.
Windows
On Windows, the implementation used the storage/SCSI pass-through path:
- device discovery through Windows device APIs
- device change handling through WM_DEVICECHANGE
- SCSI_PASS_THROUGH_DIRECT
- DeviceIoControl(… IOCTL_SCSI_PASS_THROUGH_DIRECT …)
The transfer was sector-oriented. The code used:
SPT_SECTOR_SIZE 512 SPT_MAX_PAYLOAD_SIZE 0x10000
Small or non-sector-aligned payloads had to be padded or trimmed carefully. This is one of those boring details that decides whether a system works for ten minutes or for years.
For host-to-device and device-to-host traffic, the implementation built standard SCSI READ(10) and WRITE(10) CDBs.
The private channel was not a separate vendor-specific opcode in the inspected implementation. It used standard read/write commands with a private marker in the CDB address field:
Cdb[0] = SCSIOP_READ; // or SCSIOP_WRITE
Cdb[2] = ... | 0x80; // private marker bit
That detail matters. It means the historical design was not “invent a new USB class.” It was “reuse the storage path and mark only the transfers that belong to the private channel.”
macOS
The macOS side used a completely different set of APIs:
- IOKit
- SCSITaskLib
- Disk Arbitration
- SCSITaskDeviceInterface
- exclusive access and release handling
The source comments tell a familiar story: platform behavior was not identical.
There were notes about shared open behavior, runloop source signaling, abnormal communication errors, sense data, and retry handling. The macOS version had to respect how the OS exposed CD/DVD authoring devices and SCSI tasks.
The key idea stayed the same:
Build READ(10) / WRITE(10)
Attach private marker in the CDB field
Transfer sector-aligned payloads
Use sense/status behavior for readiness and completion
The common abstraction was not the OS API. The common abstraction was SCSI.
The Device Side: Where the CD-ROM Stops Being Just a CD-ROM
On the embedded Linux side, the project had two important layers.
The first layer was the modified gadget / mass-storage behavior. It understood normal CD-ROM reads and could also detect the private command convention.
Normal host reads were served from the backing ISO image.
Private transfers were routed into application buffers instead of the backing file.
In the source, that split appears around logic like:
if normal storage command:
serve the ISO / backing file
else private command:
move data to or from the private buffer
The second layer was the user-space device communication API.
The embedded application opened a private node:
#define USBCOMM_NODE "/dev/autorun0"
usbfd = open(USBCOMM_NODE, O_RDWR);
The code used 64 KB block sizes and a larger read buffer:
UC_BLOCKSIZE = 64 * 1024
READ_BUF_SIZE = 0x40000
READ_BUF_NO = 5
It also had the unglamorous parts that make a product work:
- a background read thread
- semaphores
- disconnect flags
- buffering for leftover data
- end-of-file signaling
- write paths with failure handling
This was the bridge between the USB/SCSI world and the embedded application world.
The host thought it was issuing storage commands.
The embedded application thought it was reading and writing a device node.
The gadget layer made both views true enough to build a product on top.
The Proxy Layer: Turning the Channel into a Product Route
The private SCSI/Mass Storage channel was the foundation, but it was not the end of the project.

The next layer was a USB Proxy.
That proxy is the part that changes the story from “we found a way to move data” to “we found a route through the product.”
The proxy had two sides.
One side wrapped the USB channel through the host library:
pwUsbHostOpen(...)
pwUsbHostRead(...)
pwUsbHostWrite(...)
pwUsbHostClose(...)
The other side wrapped a socket stream:
host / domain
port
retry behavior
local bind option
stream read/write
Between them was a common stream abstraction. Incoming data from one side was forwarded into the other side:
Client / Server Protocol
|
v
Socket Stream
|
v
Proxy Bridge
- USB -> Socket
- Socket -> USB
|
v
USB Stream
|
v
SCSI / Mass Storage Transport
|
v
Embedded Device
The source shows the two directions explicitly:
- USB-to-socket forwarding
- socket-to-USB forwarding
The proxy also included the product-grade concerns that usually disappear from simplified architecture diagrams:
- USB device discovery and readiness
- wait-for-device behavior
- socket connection setup
- retry handling
- disconnect handling
- performance and traffic tracing
This is why I would not describe the project as only a USB-SCSI implementation.
The SCSI/CD-ROM path was the transport. The proxy was the product route loaded onto that transport.
In other words, the design did not merely tunnel private bytes through Mass Storage. It allowed a higher-level client/server flow to be redirected through a USB device that was constrained by endpoint budget and driverless UX requirements.
Why CD-ROM Mode Helped
CD-ROM mode had one useful property: it made the first-run product experience simple.
The device could show up as a read-only virtual disc. The host OS already knew how to mount it. The user could find the companion application without downloading anything from the network first.
The private channel reused the same Mass Storage bulk path:
EP0 setup / descriptors
Bulk IN Mass Storage data-in
Bulk OUT Mass Storage data-out
Instead of adding another interface and spending more endpoints, the design reused what was already allocated.
That was the whole point.
It was not about being clever for its own sake. It was about endpoint economics.
Readiness, Retry, and Sense Codes
The source code also shows something that does not fit nicely into architecture diagrams: the system needed a readiness protocol.
The host could ask for data before the embedded application had data ready. The device could be present while the user-space side was not ready. The cable could be unplugged. The OS could generate device notifications at inconvenient times.
So the implementation treated sense/status behavior as part of the protocol.
On Windows and macOS, the host code inspected sense data and retried on specific conditions. On the device side, logical-block or LUN-style status values were used to represent states such as “not ready yet” or “private transfer complete.”
This is the kind of detail I still look for when reading systems code.
The idea is only 20 percent of the work. The rest is making the idea survive timing, teardown, retry, and OS behavior.
What I Would Do Differently Today
If I were designing this today, I would first consider a few cleaner options:
- USB composite device with CDC-ACM for serial-style control
- HID for low-rate control traffic with very broad driverless support
- WinUSB/libusb with Microsoft OS descriptors
- WebUSB for browser-mediated flows
- a two-mode design: initial CD-ROM mode, then re-enumerate into a communication mode
I would also consider using vendor-specific SCSI opcodes instead of encoding a private marker into the address field, if the gadget layer and host permissions made that practical.
But that is a 2026 answer.
The historical design made sense for its time and constraints: limited endpoints, embedded Linux, Windows/macOS support, and a driverless first-run experience.
The Larger Engineering Lesson
The project looks like a USB trick from a distance.
Up close, it was a cross-layer integration problem:
- host OS device discovery
- Windows storage IOCTLs
- macOS IOKit and SCSI task behavior
- USB Mass Storage transport
- SCSI CDB framing
- embedded Linux gadget internals
- user-space buffering and disconnect handling
- USB stream to socket stream proxying
- client/server redirect behavior
- product installation UX
The lesson I took from it is still useful:
Before adding another layer, understand the layer you already have.
Sometimes the most reliable interface is already in the system. You only need to find the narrowest point where your intent can pass through cleanly, then build the right abstraction above it.
That pattern shows up again and again in embedded systems, graphics pipelines, Android framework work, and edge AI deployment.
The technology changes. The habit is the same:
Trace the whole stack first. Then choose the intervention point.
메타데이터
- post_id
- ca808d1f305d
- slug
- driverless-usb-proxy-over-scsi-and-cd-rom-emulation-ca808d1f305d
- url
- https://medium.com/@allenkuo/driverless-usb-proxy-over-scsi-and-cd-rom-emulation-ca808d1f305d
- canonical_url
- https://medium.com/@allenkuo/driverless-usb-proxy-over-scsi-and-cd-rom-emulation-ca808d1f305d
- author_url
- https://medium.com/@allenkuo
- status
- ok
- fetched_at
- 2026-06-17 08:20:12