moonipmi

    Experimental IPMI 2.0 RMCP+ protocol and read-only client for MoonBit native

    ipmi
    rmcp
    bmc
    network
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    2 hours ago
    Downloads
    3

    #moonipmi

    An experimental IPMI 2.0 / RMCP+ read-only client for MoonBit native applications. Protocol and session logic are written in MoonBit; a small C adapter calls OpenSSL 3 for HMAC-SHA256, raw AES-CBC, constant-time comparison and secure randomness.

    Status: an early, tested subset, not a complete IPMI implementation. Linux/macOS CI and an independent synthetic loopback BMC exchange pass. No real hardware BMC has been tested, and this is not a security-audited client. Use only on explicitly authorised systems and isolated management networks.

    #Scope

    • Cipher suite 17 only: RAKP-HMAC-SHA256 / HMAC-SHA256-128 / AES-CBC-128.
    • User privilege; verified RAKP2 and RAKP4; no anonymous access or downgrade.
    • Authenticate each packet before decryption; strict session/request correlation, checksums, lengths, padding and monotonically increasing receive sequences.
    • A single connected UDP peer, one request in flight, per-exchange deadlines, no automatic retry, explicit close and immediate local abort.
    • Three typed read-only commands: Get Device ID, Get Chassis Status, Get Sensor Reading.

    No hardware-control writes, generic network command API, OEM/SOL, SEL/FRU/SDR repository traversal, IPMI 1.5, cipher negotiation, Kg override or automatic reconnection. Set Session Privilege and Close Session are session housekeeping, not hardware configuration. Raw sensor values need SDR metadata before they can be converted into physical units. Username/password lengths are 1..16/1..20 bytes.

    #Build and test

    Requires a recent MoonBit native toolchain, a C compiler, OpenSSL 3 development files, pkg-config and Python 3. Tested locally with moon 0.1.20261104 (moonc v0.10.20+65efd5b93) and OpenSSL 3.6.3.

    # macOS brew install openssl@3 pkg-config # Debian/Ubuntu (if not already installed) sudo apt-get install libssl-dev pkg-config python3 build-essential moon update moon check --target native --deny-warn moon test --target native --deny-warn moon build --target native --deny-warn moon run examples/offline --target native python3 tools/interop.py _build/native/debug/build/examples/loopback/loopback.exe

    The last command starts a loopback-only synthetic BMC, runs the example client, verifies the handshake and all three reads, and closes the session. It never contacts a real BMC. tools/vectors.py independently regenerates the synthetic RAKP/KDF/packet vectors using Python HMAC and the OpenSSL CLI.

    #Native dependency configuration

    build.py uses MoonBit's documented experimental native prebuild configuration to discover OpenSSL through pkg-config and propagate libcrypto link settings. It only reads installed metadata: no downloads, installation or environment logs. On Homebrew macOS it can locate openssl@3 automatically. Downstream users need the same installed dependencies; they do not need machine paths or a custom compiler wrapper in their manifest. This toolchain API may change; the CI pins our tested toolchain. Both default build and an independent downstream module have been checked. The native target is required; this is not a zero-C library.

    #Use

    Import pangbit/moonipmi with alias ipmi. Inside an async function, with an explicitly authorised peer address and credentials supplied by your application:

    let client = @ipmi.Client::connect(addr, username, password, timeout_ms=2000)
    defer client.abort()
    let device = client.read(GetDeviceId).device_id()
    client.close()

    A completion-code failure is returned as Response.completion_code; typed successful-result decoders reject it. Network, framing, authentication or replay failures close the client, preventing uncertain retransmissions. abort() is idempotent and sends nothing; after it, the BMC session may remain until timeout. Install the experimental release with moon add pangbit/moonipmi@0.1.0. See the runnable checkout examples for a credential-free local demonstration.

    #Design, evidence and references

    See scope/evidence, security, and contributing. Run moon info / moon doc for API documentation.

    The protocol source is the IPMI v2.0 specification, especially §13 RMCP+, §20.1 Get Device ID, §28.2 Get Chassis Status, and §35.14 Get Sensor Reading. The comparison implementation is bougou/go-ipmi, Apache-2.0, reviewed at d4fb9c6. It is a reference, not a runtime dependency or endorsement. A go-ipmi executable comparison has not been completed. See NOTICE.

    Apache-2.0. Personally maintained; APIs may change before the first stable release.

    ProtocolError

    pub suberror ProtocolError {
    ProtocolError(String)
    } derive(
    Debug
    )

    Malformed, unsupported, mismatched, or failed IPMI exchange.

    ChassisStatus

    pub struct ChassisStatus {
    power_on : Bool
    power_overload : Bool
    power_interlock : Bool
    power_fault : Bool
    power_control_fault : Bool
    restore_policy : Int
    last_power_event : Int
    misc_status : Int
    front_panel_buttons : Int?
    } derive(Eq,
    Debug
    )

    ChassisStatus::equal

    ChassisStatus::not_equal

    fn ChassisStatus::not_equal(x : ChassisStatus, y : ChassisStatus) -> Bool

    Client

    pub struct Client {
    // private fields
    }

    Single-flight IPMI 2.0 LAN client. Only suite 17 and User privilege are supported. No password or key appears in a derived Show/Debug implementation.

    Client::abort

    fn Client::abort(self : Client) -> Unit

    Close the local socket immediately without sending a command. Safe to repeat. The BMC may retain its session until its own inactivity timeout.

    Client::close

    async fn Client::close(self : Client) -> Unit

    Close the remote session and always close the local socket. Idempotent once closed. A busy session must be aborted or allowed to complete first.

    Client::connect

    async fn Client::connect(addr :
    Addr
    , username : Bytes, password : Bytes, timeout_ms? : Int) -> Client

    Connect to one explicit peer address, authenticate RAKP2/4, and request User privilege. No anonymous access, Kg override, retry or downgrade fallback. Each exchange has a timeout (default 2000 ms, range 1..60000). Username/password are raw bytes. Nothing is written to hardware configuration.

    Client::read

    async fn Client::read(self : Client, command : ReadCommand) -> Response

    Execute one of the three explicitly read-only command variants. Any I/O, authentication, identity or framing failure closes the session.

    DeviceId

    pub struct DeviceId {
    device_id : Int
    device_revision : Int
    firmware_major : Int
    firmware_minor_bcd : Int
    firmware_available : Bool
    ipmi_major : Int
    ipmi_minor : Int
    manufacturer_id : UInt
    product_id : Int
    auxiliary_firmware : Bytes?
    } derive(Eq,
    Debug
    )

    DeviceId::equal

    fn DeviceId::equal(DeviceId, DeviceId) -> Bool

    DeviceId::not_equal

    fn DeviceId::not_equal(x : DeviceId, y : DeviceId) -> Bool

    DeviceId::to_repr

    HandshakePacket

    pub struct HandshakePacket {
    payload_type : Int
    payload : Bytes
    } derive(Eq,
    Debug
    )

    Unauthenticated RMCP+ handshake envelope. Authenticated IPMI packets use a different parser that verifies integrity before exposing plaintext.

    HandshakePacket::equal

    HandshakePacket::not_equal

    fn HandshakePacket::not_equal(x : HandshakePacket, y : HandshakePacket) -> Bool

    OpenSession

    pub struct OpenSession {
    bmc_id : UInt
    } derive(Eq,
    Debug
    )

    OpenSession::equal

    fn OpenSession::equal(OpenSession, OpenSession) -> Bool

    OpenSession::not_equal

    fn OpenSession::not_equal(x : OpenSession, y : OpenSession) -> Bool

    RakpChallenge

    pub struct RakpChallenge {
    bmc_random : Bytes
    bmc_guid : Bytes
    authentication_code : Bytes
    }

    Parsed, NOT authenticated RAKP2 challenge. Callers must authenticate it before sending RAKP3. Parsing alone does not establish a trusted session.

    ReadCommand

    pub(all) enum ReadCommand {
    GetDeviceId
    GetChassisStatus
    GetSensorReading(Byte)
    } derive(Eq,
    Debug
    )

    Read-only commands exposed by the intended high-level client. No arbitrary command dispatch is exposed by this enum.

    ReadCommand::decode

    fn ReadCommand::decode(self : ReadCommand, data : Bytes, sequence : Int) -> Response raise ProtocolError

    ReadCommand::encode

    fn ReadCommand::encode(self : ReadCommand, sequence : Int) -> Bytes raise ProtocolError

    ReadCommand::equal

    fn ReadCommand::equal(ReadCommand, ReadCommand) -> Bool

    ReadCommand::not_equal

    fn ReadCommand::not_equal(x : ReadCommand, y : ReadCommand) -> Bool

    Response

    pub struct Response {
    completion_code : Int
    data : Bytes
    } derive(Eq,
    Debug
    )

    Response::chassis_status

    fn Response::chassis_status(self : Response) -> ChassisStatus raise ProtocolError

    Response::device_id

    fn Response::device_id(self : Response) -> DeviceId raise ProtocolError

    Decode Get Device ID (Application netfn 06h, command 01h).

    Response::equal

    fn Response::equal(Response, Response) -> Bool

    Response::not_equal

    fn Response::not_equal(x : Response, y : Response) -> Bool

    Response::sensor_reading

    fn Response::sensor_reading(self : Response) -> SensorReading raise ProtocolError

    Response::to_repr

    SensorReading

    pub struct SensorReading {
    raw_reading : Int
    reading_available : Bool
    scanning_enabled : Bool
    events_enabled : Bool
    status_bits : Bytes
    } derive(Eq,
    Debug
    )

    Raw reading only. Converting it to physical units requires an SDR record.

    SensorReading::equal

    SensorReading::not_equal

    fn SensorReading::not_equal(x : SensorReading, y : SensorReading) -> Bool

    decode_handshake

    fn decode_handshake(data : Bytes) -> HandshakePacket raise ProtocolError

    decode_open_session

    fn decode_open_session(data : Bytes, tag : Int, console_id : UInt) -> OpenSession raise ProtocolError

    Reject a non-success status, wrong identity, excessive privilege, or downgrade.

    decode_rakp2

    fn decode_rakp2(data : Bytes, tag : Int, console_id : UInt) -> RakpChallenge raise ProtocolError

    decode_rakp4

    fn decode_rakp4(data : Bytes, tag : Int, console_id : UInt) -> Bytes raise ProtocolError

    Return the unverified suite 17 RAKP4 integrity value. A session may only become active after a constant-time comparison to the calculated value.

    decode_response

    fn decode_response(data : Bytes, netfn : Int, command : Int, sequence : Int) -> Response raise ProtocolError

    Validate both checksums and correlate addresses, LUNs, netfn, command and sequence. Nonzero completion codes remain structured protocol results.

    encode_handshake

    fn encode_handshake(payload_type : Int, payload : Bytes) -> Bytes raise ProtocolError

    encode_request

    fn encode_request(netfn : Int, command : Int, sequence : Int, data : Bytes) -> Bytes raise ProtocolError

    IPMI LAN request. Sequence is a six-bit value; data is bounded to 1024 bytes. This low-level encoder does not transmit anything.

    open_session_request

    fn open_session_request(tag : Int, console_id : UInt) -> Bytes raise ProtocolError

    Open a User-privilege session with cipher suite 17 only: RAKP-HMAC-SHA256, HMAC-SHA256-128, AES-CBC-128. No downgrade negotiation.

    rakp1_request

    fn rakp1_request(tag : Int, bmc_id : UInt, console_random : Bytes, username : Bytes) -> Bytes raise ProtocolError

    Fixed User privilege (02h), name-and-privilege lookup (bit 4 clear). Username is an opaque sequence of 1..16 bytes, not a locale-dependent string.

    rakp3_request

    fn rakp3_request(tag : Int, bmc_id : UInt, authentication_code : Bytes) -> Bytes raise ProtocolError

    Encode a suite 17 RAKP3 packet. Authentication code must already have been calculated after successful RAKP2 verification. This function does not verify it.