1# IPMI Architecture 2 3IPMI is made up of commands and channels. Commands are provided by providers and 4channels are processes called bridges. Commands are received from external 5sources (system admin users) via the bridges and routed to the providers' 6command handlers via the main IPMI daemon. 7 8## High-Level Overview 9 10IPMI is all about commands and responses. Channels provide a mechanism for 11transporting the data, each with a slightly different protocol and transport 12layer, but ultimately, the highest level data is a raw IPMI command consisting 13of a NetFn/LUN, Command, and optional data. Each response is likewise a 14Completion Code and optional data. So the first step is to break apart channels 15and the IPMI queue. 16 17``` 18 /------------------\ 19 /----------------------------\ | | 20 | KCS/BT - Host | <-All IPMI cmds-> | | 21 | | | | 22 \----------------------------/ | IPMI Daemon | 23 | (ipmid) | 24 | | 25 /-----------------------------\ | | 26 | LAN - RMCP+ | | | 27 | /--------------------------\| | | 28 | |*Process the Session and || <-All IPMI cmds-> | | 29 | | SOL commands. || except session | | 30 | |*Create Session Objs || and SOL cmds | | 31 | \--------------------------/| | | 32 \-----------------------------/ \------------------/ 33 : ^ ^ 34 : | | 35 : | | 36 /-------------------------\ | | 37 | Active session/SOL Objs | <---------Query the session-/ | 38 | - Properties | and SOL data via Dbus | 39 \-------------------------/ | 40 V 41 ---------------------\ 42 | D-Bus services | 43 | ------------------ | 44 | Do work on behalf | 45 | of IPMI commands | 46 ---------------------/ 47``` 48 49The IPMI messages that get passed to and from the IPMI daemon (ipmid) are 50basically the equivalent of ipmitool's "raw" commands with a little more 51information about the message. 52 53```less 54Message Data: 55 byte:LUN - LUN from netFn/LUN pair (0-3, as per the IPMI spec) 56 byte:netFn - netFn from netFn/LUN pair (as per the IPMI spec) 57 byte:cmd - IPMI command ID (as per the IPMI spec) 58 array<byte>:data - optional command data (as per the IPMI spec) 59 dict<string:variant>:options - optional additional meta-data 60 "userId":int - IPMI user ID of caller (0 for session-less channels) 61 "privilege": enum:privilege - ADMIN, USER, OPERATOR, CALLBACK; 62 must be less than or equal to the privilege of the user and less 63 than or equal to the max privilege of this channel 64``` 65 66```less 67Response Data: 68 byte:CC - IPMI completion code 69 array<byte>:data - optional response data 70``` 71 72A channel bridge, upon receiving a new IPMI command, will extract the necessary 73fields from whatever transport format (RMCP+, IPMB, KCS, etc.) For session-based 74channels (RMCP+) the bridge is responsible for establishing the session with 75credentials and determining the maximum privilege available for this session. 76The bridge then takes the extracted command, data, and possibly user and 77privilge information, and encodes them in a D-Bus method call to send to the 78IPMI daemon, ipmid. The daemon will take the message, and attempt to find an 79appropriate handler in its handler tables. If a handler is found, it will 80attempt to extract the required parameters for the handler and pass them along. 81The handler will return a tuple of response parameters, which will get packed 82back into a D-Bus response message and sent back to the calling channel's 83bridge. The bridge will then re-package the response into its transport protocol 84and send it off. 85 86The next part is to provide a higher-level, strongly-typed, modern C++ mechanism 87for registering handlers. Each handler will specify exactly what arguments are 88needed for the command and what types will be returned in the response. This 89way, the ipmid queue can unpack requests and pack responses in a safe manner. 90Because the handler packing and unpacking code is templated, it is written 91mostly in headers. 92 93## Details and Implementation 94 95For session-less channels (like BT, KCS, and IPMB), the only privilege check 96will be to see that the requested privilege is less than or equal to the 97channel's maximum privilege. If the channel has a session and authenticates 98users, the privilege must be less than or equal to the channel's maximum 99privilege and the user's maximum privilege. 100 101Ipmid takes the LUN/netFN/Cmd tuple and looks up the corresponding handler 102function. If the requested privilege is less than or equal to the required 103privilege for the given registered command, the request may proceed. If any of 104these checks fail, ipmid returns with _Insufficient Privilege_. 105 106At this point, the IPMI command is run through the filter hooks. The default 107hook is ACCEPT, where the command just passes onto the execution phase. But OEMs 108and providers can register hooks that would ultimately block IPMI commands from 109executing, much like the IPMI 2.0 Spec's Firmware Firewall. The hook would be 110passed in the context of the IPMI call and the raw content of the call and has 111the opportunity to return any valid IPMI completion code. Any non-zero 112completion code would prevent the command from executing and would be returned 113to the caller. 114 115The network channel bridges (netipmid), executing one process per interface, 116handle session (RMCP+) and SOL commands and responses to those commands. Get/Set 117SOL configuration, Get session info, close session commands can also be 118requested through KCS/BT interface, ipmid daemon must also need details about 119session and SOL. In order to maintain sync between ipmid and netipmid daemon, 120session and SOL are exposed in D-Bus, which ipmid can query and respond to 121commands issued through host interface (KCS/BT). 122 123The next phase is parameter unpacking and validation. This is done by 124compiler-generated code with variadic templates at handler registration time. 125The registration function is a templated function that allows any type of 126handler to be passed in so that the types of the handler can be extracted and 127unpacked. 128 129This is done in the core IPMI library (from a high level) like this: 130 131```cpp 132template <typename Handler> 133class IpmiHandler 134{ 135 public: 136 explicit IpmiHandler(Handler&& handler) : 137 handler_(std::forward<Handler>(handler)) 138 { 139 } 140 message::Response::ptr call(message::Request::ptr request) 141 { 142 message::Response::ptr response = request->makeResponse(); 143 // < code to deduce UnpackArgsType and ResultType from Handler > 144 UnpackArgsType unpackArgs; 145 ipmi::Cc unpackError = request->unpack(unpackArgs); 146 if (unpackError != ipmi::ccSuccess) 147 { 148 response->cc = unpackError; 149 return response; 150 } 151 ResultType result = std::apply(handler_, unpackArgs); 152 response->cc = std::get<0>(result); 153 auto payload = std::get<1>(result); 154 response->pack(*payload); 155 return response; 156 } 157 private: 158 Handler handler_; 159}; 160 ... 161 namespace ipmi { 162 constexpr NetFn netFnApp = 0x06; 163 namespace app { 164 constexpr Cmd cmdSetUserAccessCommand = 0x43; 165 } 166 class Context { 167 NetFn netFn; 168 Cmd cmd; 169 int channel; 170 int userId; 171 Privilege priv; 172 boost::asio::yield_context* yield; // for async operations 173 }; 174 } 175``` 176 177While some IPMI handlers would look like this: 178 179```cpp 180ipmi::RspType<> setUserAccess( 181 std::shared_ptr<ipmi::Context> context, 182 uint4_t channelNumber, 183 bool ipmiEnable, 184 bool linkAuth, 185 bool callbackRestricted, 186 bool changeBit, 187 uint6_t userId, 188 uint2_t reserved1, 189 uint4_t privLimit, 190 uint4_t reserved2, 191 std::optional<uint4_t> userSessionLimit, 192 std::optional<uint4_t> reserved3) { 193 ... 194 return ipmi::ResponseSuccess(); 195} 196 197ipmi::RspType<uint8_t, // max user IDs 198 uint6_t, // count of enabled user IDs 199 uint2_t, // user ID enable status 200 uint8_t, // count of fixed user IDs 201 uint4_t // user privilege for given channel 202 bool, // ipmi messaging enabled 203 bool, // link authentication enabled 204 bool, // callback access 205 bool, // reserved bit 206 > 207 getUserAccess(std::shared_ptr<ipmi::Context> context, uint8_t channelNumber, 208 uint8_t userId) 209{ 210 if (<some error condition>) 211 { 212 return ipmi::response(ipmi::ccParmOutOfRange); 213 } 214 // code to get 215 const auto& [usersMax, status, usersEnabled, usersFixed, access, priv] = 216 getSdBus()->yield_method_call(*context->yield, ...); 217 return ipmi::responseSuccess(usersMax, usersEnabled, status, usersFixed, 218 priv, access.messaging, access.linkAuth, 219 access.callback, false); 220} 221 222void providerInitFn() 223{ 224 ipmi::registerHandler(ipmi::prioOpenBmcBase, ipmi::netFnApp, 225 ipmi::app::cmdSetUserAccessCommand, 226 ipmi::Privilege::Admin, 227 setUserAccess); 228 ipmi::registerHandler(ipmi::prioOpenBmcBase, ipmi::netFnApp, 229 ipmi::app::cmdGetUserAccessCommand, 230 ipmi::Privilege::Operator, 231 getUserAccess); 232} 233``` 234 235Ipmid providers are all executed as boost::asio::coroutines. This means that 236they can take advantage of any of the boost::asio async method calls in a way 237that looks like synchronous code, but will execute asynchronously by yielding 238control of the processor to the run loop via the yield_context object. Use the 239yield_context object by passing it as a 'callback' which will then cause the 240calling coroutine to yield the processor until its IO is ready, at which point 241it will 'return' much like a synchronous call. 242 243Ideally, all handlers would take advantage of the asynchronous capabilities of 244ipmid via the boost::asio::yield_context. This means that the queue could have 245multiple in-flight calls that are waiting on another D-Bus call to return. With 246asynchronous calls, this will not block the rest of the queue's operation, The 247sdbusplus::asio::connection that is available to all providers via the 248getSdBus() function provides yield_method_call() which is an asynchronous D-Bus 249call mechanism that 'looks' like a synchronous call. It is important that any 250global data that an asynchronous handler uses is protected as if the handler is 251multi-threaded. Since many of the resources used in IPMI handlers are actually 252D-Bus objects, this is not likely a common issue because of the serialization 253that happens via the D-Bus calls. 254 255Using templates, it is possible to extract the return type and argument types of 256the handlers and use that to unpack (and validate) the arguments from the 257incoming request and then pack the result back into a vector of bytes to send 258back to the caller. The deserializer will keep track of the number of bits it 259has unpacked and then compare it with the total number of bits that the method 260is requesting. In the example, we are assuming that the non-full-byte integers 261are packed bits in the message in most-significant-bit first order (same order 262the specification describes them in). Optional arguments can be used easily with 263C++17's std::optional. 264 265Some types that are supported are as follows: 266 267- standard integer types (uint8_t, uint16_t, uint32_t, int, long, etc.) 268- bool (extracts a single bit, same as uint1_t) 269- multi-precision integers (uint<N>) 270- std::bitset<N> 271- std::optional<T> - extracts T if there are enough remaining bits/bytes 272- std::array<T, N> - extracts N elements of T if possible 273- std::vector<T> - extracts elements of T from the remaining bytes 274- any additional types can be created by making a pack/unpack template 275 276For partial byte types, the least-significant bits of the next full byte are 277extracted first. While this is opposite of the order the IPMI specification is 278written, it makes the code simple. Multi-byte fields are extracted as LSByte 279first (little-endian) to match the IPMI specification. 280 281When returning partial byte types, they are also packed into the reply in the 282least-significant bit first order. As each byte fills up, the full byte gets 283pushed onto the byte stream. For examples of how the packing and unpacking is 284used, see the unit test cases in test/message/pack.cpp and 285test/message/unpack.cpp. 286 287As an example this is how a bitset is unpacked 288 289```cpp 290/** @brief Specialization of UnpackSingle for std::bitset<N> 291 */ 292template <size_t N> 293struct UnpackSingle<std::bitset<N>> 294{ 295 static int op(Payload& p, std::bitset<N>& t) 296 { 297 static_assert(N <= (details::bitStreamSize - CHAR_BIT)); 298 size_t count = N; 299 // acquire enough bits in the stream to fulfill the Payload 300 if (p.fillBits(count)) 301 { 302 return -1; 303 } 304 fixed_uint_t<details::bitStreamSize> bitmask = ((1 << count) - 1); 305 t |= (p.bitStream & bitmask).convert_to<unsigned long long>(); 306 p.bitStream >>= count; 307 p.bitCount -= count; 308 return 0; 309 } 310}; 311``` 312 313If a handler needs to unpack a variable payload, it is possible to request the 314Payload parameter. When the Payload parameter is present, any remaining, 315unpacked bytes are available for inspection. Using the same unpacking calls that 316were used to unpack the other parameters, the remaining parameters can be 317manually unpacked. This is helpful for multi-function commands like Set LAN 318Configuration Parameters, where the first two bytes are fixed, but the remaining 3193:N bytes vary based on the selected parameter. 320