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