Move protocol and SCSI code into importable mount/ package with a single public API (Config + Run). Restructure as multi-binary repo with cmd/aten-mount/ CLI wrapper using context-based cancellation. Add Nix flake with go_1_26 for builds and devshell. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
359 lines
10 KiB
Go
359 lines
10 KiB
Go
package mount
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rc4"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"time"
|
|
)
|
|
|
|
// PDU types
|
|
const (
|
|
pduPlugIn = 0x01 // Client -> BMC
|
|
pduMountStatus = 0x02 // BMC -> Client
|
|
pduKeepAliveResp = 0x03 // Client -> BMC
|
|
pduKeepAliveReq = 0x04 // BMC -> Client
|
|
pduUnmount = 0x05 // Client -> BMC
|
|
pduUnmountStatus = 0x06 // BMC -> Client
|
|
pduSetEP = 0x07 // Client -> BMC
|
|
)
|
|
|
|
var rc4Key = []byte("BX80570E3110Q814A447")
|
|
|
|
// USB descriptors for a virtual CD-ROM device.
|
|
// Assembled from vuDevRespData in the original binary.
|
|
// Device descriptor: USB 2.0, Mass Storage, vendor 0x0EA0, product 0x2222
|
|
var usbDeviceDescriptor = []byte{
|
|
0x12, // bLength
|
|
0x01, // bDescriptorType (DEVICE)
|
|
0x00, 0x02, // bcdUSB (2.0)
|
|
0x00, // bDeviceClass
|
|
0x00, // bDeviceSubClass
|
|
0x00, // bDeviceProtocol
|
|
0x40, // bMaxPacketSize0 (64)
|
|
0xA0, 0x0E, // idVendor (0x0EA0)
|
|
0x22, 0x22, // idProduct (0x2222, descriptor_mode=0)
|
|
0x00, 0x02, // bcdDevice (2.0)
|
|
0x00, // iManufacturer
|
|
0x00, // iProduct
|
|
0x00, // iSerialNumber
|
|
0x01, // bNumConfigurations
|
|
}
|
|
|
|
// Config + Interface + 3 Endpoints for a single Mass Storage interface
|
|
var usbConfigDescriptor = []byte{
|
|
// Configuration Descriptor
|
|
0x09, // bLength
|
|
0x02, // bDescriptorType (CONFIGURATION)
|
|
0x00, 0x00, // wTotalLength (filled in below)
|
|
0x01, // bNumInterfaces
|
|
0x01, // bConfigurationValue
|
|
0x00, // iConfiguration
|
|
0x80, // bmAttributes (Bus-powered)
|
|
0x64, // bMaxPower (200mA)
|
|
// Interface Descriptor
|
|
0x09, // bLength
|
|
0x04, // bDescriptorType (INTERFACE)
|
|
0x00, // bInterfaceNumber
|
|
0x00, // bAlternateSetting
|
|
0x03, // bNumEndpoints
|
|
0x08, // bInterfaceClass (Mass Storage)
|
|
0x06, // bInterfaceSubClass (SCSI transparent)
|
|
0x50, // bInterfaceProtocol (Bulk-Only)
|
|
0x00, // iInterface
|
|
// Endpoint 1: Bulk OUT
|
|
0x07, // bLength
|
|
0x05, // bDescriptorType (ENDPOINT)
|
|
0x01, // bEndpointAddress (EP1 OUT)
|
|
0x02, // bmAttributes (Bulk)
|
|
0x00, 0x02, // wMaxPacketSize (512)
|
|
0xFF, // bInterval
|
|
// Endpoint 2: Bulk IN
|
|
0x07, // bLength
|
|
0x05, // bDescriptorType (ENDPOINT)
|
|
0x82, // bEndpointAddress (EP2 IN)
|
|
0x02, // bmAttributes (Bulk)
|
|
0x00, 0x02, // wMaxPacketSize (512)
|
|
0xFF, // bInterval
|
|
// Endpoint 3: Interrupt IN
|
|
0x07, // bLength
|
|
0x05, // bDescriptorType (ENDPOINT)
|
|
0x83, // bEndpointAddress (EP3 IN)
|
|
0x03, // bmAttributes (Interrupt)
|
|
0x02, 0x00, // wMaxPacketSize (2)
|
|
0x01, // bInterval
|
|
}
|
|
|
|
// String descriptor 0: Language ID
|
|
var usbStringLang = []byte{0x04, 0x03, 0x09, 0x04} // English (US)
|
|
|
|
// String descriptor 1: Product name "Flash Disk" (UTF-16LE)
|
|
var usbStringProduct = []byte{
|
|
0x22, // bLength (34)
|
|
0x03, // bDescriptorType (STRING)
|
|
'F', 0, 'l', 0, 'a', 0, 's', 0, 'h', 0, ' ', 0,
|
|
'D', 0, 'i', 0, 's', 0, 'k', 0,
|
|
' ', 0, ' ', 0, ' ', 0, ' ', 0, ' ', 0, ' ', 0,
|
|
}
|
|
|
|
// String descriptor 2: Serial number "4E8F092C3FD7F8F7" (UTF-16LE)
|
|
var usbStringSerial = []byte{
|
|
0x22, // bLength (34)
|
|
0x03, // bDescriptorType (STRING)
|
|
'4', 0, 'E', 0, '8', 0, 'F', 0, '0', 0, '9', 0, '2', 0, 'C', 0,
|
|
'3', 0, 'F', 0, 'D', 0, '7', 0, 'F', 0, '8', 0, 'F', 0, '7', 0,
|
|
}
|
|
|
|
func buildPlugInPacket(username, password string) []byte {
|
|
// Build USB config descriptor with correct total length
|
|
configDesc := make([]byte, len(usbConfigDescriptor))
|
|
copy(configDesc, usbConfigDescriptor)
|
|
totalLen := len(configDesc)
|
|
configDesc[2] = byte(totalLen)
|
|
configDesc[3] = byte(totalLen >> 8)
|
|
|
|
// Assemble USB descriptors block:
|
|
// Each entry: 1 byte length + descriptor data
|
|
// Entry 0: Device descriptor
|
|
// Entry 1: Config descriptor (with interfaces/endpoints)
|
|
// Entry 2: Language string
|
|
// Entry 3: Product string
|
|
// Entry 4: Serial string
|
|
var descs []byte
|
|
appendDesc := func(d []byte) {
|
|
descs = append(descs, byte(len(d)))
|
|
descs = append(descs, d...)
|
|
}
|
|
appendDesc(usbDeviceDescriptor)
|
|
appendDesc(configDesc)
|
|
appendDesc(usbStringLang)
|
|
appendDesc(usbStringProduct)
|
|
appendDesc(usbStringSerial)
|
|
|
|
// Build the fixed portion of the PlugIn packet (0x34 = 52 bytes)
|
|
pkt := make([]byte, 52+len(descs))
|
|
pkt[0] = 0x00
|
|
pkt[1] = 0x80 // RC4 encryption enabled
|
|
pkt[2] = 0x00
|
|
pkt[3] = 0x01 // plug-in type
|
|
pkt[4] = 0x2C // base length (44)
|
|
// bytes 5-7: reserved zeros
|
|
|
|
// Username at offset 8 (16 bytes, null-padded)
|
|
copy(pkt[8:24], make([]byte, 16))
|
|
if len(username) > 16 {
|
|
copy(pkt[8:24], username[:16])
|
|
} else {
|
|
copy(pkt[8:], username)
|
|
}
|
|
|
|
// Password at offset 24 (20 bytes, null-padded)
|
|
copy(pkt[24:44], make([]byte, 20))
|
|
if len(password) > 20 {
|
|
copy(pkt[24:44], password[:20])
|
|
} else {
|
|
copy(pkt[24:], password)
|
|
}
|
|
|
|
// SID bytes at offset 44 (4 bytes, zeros - no SID auth)
|
|
// Device config at offset 48: (devIdx+1)*2 = 0x02 for devIdx=0
|
|
pkt[48] = 0x02
|
|
// Media type at offset 49: 2 = CD-ROM
|
|
pkt[49] = 0x02
|
|
// Reserved at 50-51: zeros
|
|
|
|
// USB descriptors at offset 52
|
|
copy(pkt[52:], descs)
|
|
|
|
// RC4 encrypt bytes 8 onward
|
|
cipher, _ := rc4.NewCipher(rc4Key)
|
|
cipher.XORKeyStream(pkt[8:], pkt[8:])
|
|
|
|
return pkt
|
|
}
|
|
|
|
func readMountStatus(conn net.Conn) (byte, error) {
|
|
// Read 8-byte PDU header + 1 status byte = 9 bytes
|
|
// Expected: 00 00 00 02 09 00 00 00 SS
|
|
buf := make([]byte, 9)
|
|
if _, err := io.ReadFull(conn, buf); err != nil {
|
|
return 0, fmt.Errorf("read mount status: %w", err)
|
|
}
|
|
pduType := binary.BigEndian.Uint32(buf[0:4])
|
|
if pduType != pduMountStatus {
|
|
return 0, fmt.Errorf("expected mount status (0x02), got 0x%02x", pduType)
|
|
}
|
|
return buf[8], nil
|
|
}
|
|
|
|
func mountStatusString(status byte) string {
|
|
switch status {
|
|
case 0x00:
|
|
return "success"
|
|
case 0x01:
|
|
return "authentication failure"
|
|
case 0x02:
|
|
return "system busy"
|
|
case 0x03:
|
|
return "privilege error"
|
|
case 0x04:
|
|
return "virtual media is in detach mode"
|
|
case 0x05:
|
|
return "session ID expired"
|
|
case 0x06:
|
|
return "BMC is in firmware update"
|
|
default:
|
|
return fmt.Sprintf("unknown status 0x%02x", status)
|
|
}
|
|
}
|
|
|
|
func buildSetEPPacket() []byte {
|
|
// 00 00 00 07 08 00 00 00 06 03 01 10 02 20 03 30
|
|
// Header: type=0x07
|
|
// Length: num_endpoints * 2 + 2 = 3*2+2 = 8
|
|
// Payload: subclass=6(SCSI), num_eps=3, then 3 endpoint pairs
|
|
pkt := make([]byte, 16)
|
|
binary.BigEndian.PutUint32(pkt[0:4], pduSetEP)
|
|
binary.LittleEndian.PutUint32(pkt[4:8], 8)
|
|
pkt[8] = 0x06 // SCSI transparent command set
|
|
pkt[9] = 0x03 // 3 endpoints
|
|
pkt[10] = 0x01 // EP1 type
|
|
pkt[11] = 0x10 // EP1 addr
|
|
pkt[12] = 0x02 // EP2 type
|
|
pkt[13] = 0x20 // EP2 addr
|
|
pkt[14] = 0x03 // EP3 type
|
|
pkt[15] = 0x30 // EP3 addr
|
|
return pkt
|
|
}
|
|
|
|
func buildKeepAliveResponse() []byte {
|
|
// 00 00 00 03 04 00 00 00 FF FF FF FF
|
|
pkt := make([]byte, 12)
|
|
binary.BigEndian.PutUint32(pkt[0:4], pduKeepAliveResp)
|
|
binary.LittleEndian.PutUint32(pkt[4:8], 4)
|
|
pkt[8] = 0xFF
|
|
pkt[9] = 0xFF
|
|
pkt[10] = 0xFF
|
|
pkt[11] = 0xFF
|
|
return pkt
|
|
}
|
|
|
|
func buildUnmountPacket() []byte {
|
|
// 00 00 00 05 00 00 00 00
|
|
pkt := make([]byte, 8)
|
|
binary.BigEndian.PutUint32(pkt[0:4], pduUnmount)
|
|
return pkt
|
|
}
|
|
|
|
// buildDataPDU builds the data PDU tag for SCSI response data.
|
|
// Format: 22 00 EP 00 LL LL LL LL [data]
|
|
func buildDataPDU(ep byte, data []byte) []byte {
|
|
pdu := make([]byte, 8+len(data))
|
|
pdu[0] = 0x22
|
|
pdu[1] = 0x00
|
|
pdu[2] = ep
|
|
pdu[3] = 0x00
|
|
binary.LittleEndian.PutUint32(pdu[4:8], uint32(len(data)))
|
|
copy(pdu[8:], data)
|
|
return pdu
|
|
}
|
|
|
|
// buildCSW builds the Command Status Wrapper response.
|
|
// Format: 22 00 EP FF 0D 00 00 00 USBS TAG. RES. STATUS
|
|
func buildCSW(ep byte, tag [4]byte, dataTransferLen uint32, actualLen uint32, status byte) []byte {
|
|
pdu := make([]byte, 21) // 8 tag + 13 body
|
|
pdu[0] = 0x22
|
|
pdu[1] = 0x00
|
|
pdu[2] = ep
|
|
pdu[3] = 0xFF
|
|
binary.LittleEndian.PutUint32(pdu[4:8], 13)
|
|
// CSW body
|
|
pdu[8] = 'U' // dCSWSignature "USBS"
|
|
pdu[9] = 'S'
|
|
pdu[10] = 'B'
|
|
pdu[11] = 'S'
|
|
copy(pdu[12:16], tag[:]) // dCSWTag
|
|
var residue uint32
|
|
if dataTransferLen > actualLen {
|
|
residue = dataTransferLen - actualLen
|
|
}
|
|
binary.LittleEndian.PutUint32(pdu[16:20], residue) // dCSWDataResidue
|
|
pdu[20] = status // bCSWStatus
|
|
return pdu
|
|
}
|
|
|
|
// PDU header as read from the connection
|
|
type pduHeader struct {
|
|
typeOrTag [4]byte // first 4 bytes (big-endian PDU type, or data tag)
|
|
length uint32 // second 4 bytes (little-endian)
|
|
}
|
|
|
|
func readPDUHeader(conn net.Conn) (pduHeader, error) {
|
|
var buf [8]byte
|
|
if _, err := io.ReadFull(conn, buf[:]); err != nil {
|
|
return pduHeader{}, err
|
|
}
|
|
var h pduHeader
|
|
copy(h.typeOrTag[:], buf[0:4])
|
|
h.length = binary.LittleEndian.Uint32(buf[4:8])
|
|
return h, nil
|
|
}
|
|
|
|
func (h pduHeader) pduType() uint32 {
|
|
return binary.BigEndian.Uint32(h.typeOrTag[:])
|
|
}
|
|
|
|
// commandLoop reads PDU headers and dispatches commands until context
|
|
// cancellation or error.
|
|
func commandLoop(ctx context.Context, conn net.Conn, dev *scsiDevice) error {
|
|
for {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil
|
|
}
|
|
|
|
// Set read deadline so we can check context periodically
|
|
conn.SetReadDeadline(time.Now().Add(1 * time.Second))
|
|
hdr, err := readPDUHeader(conn)
|
|
if err != nil {
|
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
|
continue // timeout, loop back to check context
|
|
}
|
|
return fmt.Errorf("read PDU header: %w", err)
|
|
}
|
|
|
|
// Dispatch based on PDU type or CBW indicator
|
|
if hdr.length == 0x1F {
|
|
// CBW: read 31 bytes
|
|
ep := hdr.typeOrTag[2]
|
|
var cbwBuf [31]byte
|
|
if _, err := io.ReadFull(conn, cbwBuf[:]); err != nil {
|
|
return fmt.Errorf("read CBW: %w", err)
|
|
}
|
|
resp := dev.handleCBW(ep, cbwBuf[:])
|
|
if _, err := conn.Write(resp); err != nil {
|
|
return fmt.Errorf("write SCSI response: %w", err)
|
|
}
|
|
} else {
|
|
switch hdr.pduType() {
|
|
case pduKeepAliveReq:
|
|
if _, err := conn.Write(buildKeepAliveResponse()); err != nil {
|
|
return fmt.Errorf("write keepalive: %w", err)
|
|
}
|
|
case pduUnmountStatus:
|
|
log.Printf("BMC sent unmount notification")
|
|
return nil
|
|
case pduMountStatus:
|
|
// Can happen if BMC re-sends status; read the status byte
|
|
var status [1]byte
|
|
io.ReadFull(conn, status[:])
|
|
log.Printf("Received mount status: 0x%02x", status[0])
|
|
default:
|
|
log.Printf("Unknown PDU type 0x%08x, length %d", hdr.pduType(), hdr.length)
|
|
}
|
|
}
|
|
}
|
|
}
|