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