aten-ipmi-tools/mount/protocol.go
Davíð Steinn Geirsson 826edc817a refactor: extract mount library package, add flake.nix
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>
2026-03-11 14:28:22 +00:00

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)
}
}
}
}