Skip to main content

ipmi_rs/
file.rs

1use std::fmt::{Display, Formatter};
2use std::{
3    ffi::c_int,
4    io,
5    os::fd::{AsFd, AsRawFd},
6    time::Duration,
7};
8
9use ipmi_rs_core::connection::NetFn;
10use nix::errno::Errno;
11use nix::poll::{PollFd, PollFlags};
12
13use crate::connection::{
14    Address, IpmiConnection, Message, Request, RequestTargetAddress, Response,
15};
16
17#[repr(C)]
18#[derive(Debug)]
19pub struct IpmiMessage {
20    netfn: u8,
21    cmd: u8,
22    data_len: u16,
23    data: *mut u8,
24}
25
26impl IpmiMessage {
27    fn data(&self) -> &[u8] {
28        unsafe { core::slice::from_raw_parts(self.data, self.data_len as usize) }
29    }
30}
31
32impl IpmiMessage {
33    fn log(&self, level: log::Level) {
34        log::log!(level, "  NetFn      = 0x{:02X}", self.netfn);
35        log::log!(level, "  Command    = 0x{:02X}", self.cmd);
36        log::log!(level, "  Data len   = {}", self.data_len);
37        if self.data_len > 0 {
38            log::log!(level, "  Data       = {:02X?}", self.data());
39        }
40    }
41}
42
43#[repr(C)]
44#[derive(Debug)]
45pub struct IpmiRequest {
46    addr: *mut u8,
47    addr_len: u32,
48    msg_id: i64,
49    message: IpmiMessage,
50}
51
52impl IpmiRequest {
53    pub fn log(&self, level: log::Level) {
54        log::log!(level, "  Message ID = 0x{:02X}", self.msg_id);
55        self.message.log(level);
56    }
57}
58
59#[repr(C)]
60#[derive(Debug)]
61pub struct IpmiRecv {
62    recv_type: i32,
63    addr: *mut u8,
64    addr_len: u32,
65    msg_id: i64,
66    message: IpmiMessage,
67}
68
69impl IpmiRecv {
70    fn log(&self, level: log::Level) {
71        log::log!(level, "  Type       = 0x{:02X}", self.recv_type);
72        log::log!(level, "  Message ID = 0x{:02X}", self.msg_id);
73        self.message.log(level);
74    }
75}
76
77#[derive(Clone, Copy, Debug)]
78pub enum CreateResponseError {
79    NotAResponse,
80    NotEnoughData,
81    InvalidCmd,
82}
83
84impl TryFrom<IpmiRecv> for Response {
85    type Error = CreateResponseError;
86
87    fn try_from(value: IpmiRecv) -> Result<Self, Self::Error> {
88        let (netfn, cmd) = (value.message.netfn, value.message.cmd);
89
90        let netfn_parsed = NetFn::from(netfn);
91
92        if netfn_parsed.response_value() == netfn {
93            let message = Message::new_raw(netfn, cmd, value.message.data().to_vec());
94            let response =
95                Response::new(message, value.msg_id).ok_or(CreateResponseError::NotEnoughData)?;
96            Ok(response)
97        } else {
98            Err(CreateResponseError::NotAResponse)
99        }
100    }
101}
102
103mod ioctl {
104    const IPMI_IOC_MAGIC: u8 = b'i';
105
106    use nix::{ioctl_read, ioctl_readwrite};
107
108    use super::{IpmiRecv, IpmiRequest};
109
110    ioctl_readwrite!(ipmi_recv_msg_trunc, IPMI_IOC_MAGIC, 11, IpmiRecv);
111    ioctl_read!(ipmi_send_request, IPMI_IOC_MAGIC, 13, IpmiRequest);
112    ioctl_read!(ipmi_get_my_address, IPMI_IOC_MAGIC, 18, u32);
113}
114
115#[repr(C)]
116enum IpmiAddr {
117    SysIface(IpmiSysIfaceAddr),
118    Ipmb(IpmiIpmbAddr),
119}
120
121impl IpmiAddr {
122    fn ptr(&mut self) -> *mut u8 {
123        match self {
124            IpmiAddr::SysIface(ref mut bmc_addr) => std::ptr::addr_of_mut!(*bmc_addr) as *mut u8,
125            IpmiAddr::Ipmb(ref mut ipmb_addr) => std::ptr::addr_of_mut!(*ipmb_addr) as *mut u8,
126        }
127    }
128    fn size(&self) -> u32 {
129        match self {
130            IpmiAddr::SysIface(_) => core::mem::size_of::<IpmiSysIfaceAddr>() as u32,
131            IpmiAddr::Ipmb(_) => core::mem::size_of::<IpmiIpmbAddr>() as u32,
132        }
133    }
134}
135
136impl From<RequestTargetAddress> for IpmiAddr {
137    fn from(value: RequestTargetAddress) -> Self {
138        match value {
139            RequestTargetAddress::Bmc(lun) => {
140                IpmiAddr::SysIface(IpmiSysIfaceAddr::bmc(lun.value()))
141            }
142            RequestTargetAddress::BmcOrIpmb(addr, channel, lun) => IpmiAddr::Ipmb(
143                IpmiIpmbAddr::new(channel.value() as i16, addr.0, lun.value()),
144            ),
145        }
146    }
147}
148
149impl Display for IpmiAddr {
150    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
151        match self {
152            IpmiAddr::SysIface(addr) => {
153                write!(f, "System interface (LUN: {})", addr.lun)
154            }
155            IpmiAddr::Ipmb(addr) => {
156                write!(
157                    f,
158                    "IPMB target (Channel: {}, Target: {}, LUN: {})",
159                    addr.channel, addr.target_addr, addr.lun
160                )
161            }
162        }
163    }
164}
165
166#[repr(C)]
167#[derive(Clone, Debug, PartialEq)]
168pub struct IpmiSysIfaceAddr {
169    ty: i32,
170    channel: i16,
171    lun: u8,
172}
173
174#[repr(C)]
175#[derive(Clone, Debug, PartialEq)]
176pub struct IpmiIpmbAddr {
177    ty: i32,
178    channel: i16,
179    target_addr: u8,
180    lun: u8,
181}
182
183impl IpmiIpmbAddr {
184    const IPMI_IPMB_ADDR_TYPE: i32 = 0x01;
185
186    pub const fn new(channel: i16, target_addr: u8, lun: u8) -> Self {
187        Self {
188            ty: Self::IPMI_IPMB_ADDR_TYPE,
189            channel,
190            target_addr,
191            lun,
192        }
193    }
194}
195
196impl IpmiSysIfaceAddr {
197    const IPMI_SYSTEM_INTERFACE_ADDR_TYPE: i32 = 0x0c;
198    const IPMI_BMC_CHANNEL: i16 = 0xf;
199
200    pub const fn bmc(lun: u8) -> Self {
201        Self {
202            ty: Self::IPMI_SYSTEM_INTERFACE_ADDR_TYPE,
203            channel: Self::IPMI_BMC_CHANNEL,
204            lun,
205        }
206    }
207}
208
209pub struct File {
210    inner: std::fs::File,
211    recv_timeout: Duration,
212    seq: i64,
213    my_addr: Address,
214}
215
216impl File {
217    fn fd(&mut self) -> c_int {
218        self.inner.as_raw_fd()
219    }
220
221    pub fn new(path: impl AsRef<std::path::Path>, recv_timeout: Duration) -> io::Result<Self> {
222        let mut inner = std::fs::File::open(path)?;
223
224        let my_addr = match Self::load_my_address_from_file(&mut inner) {
225            Ok(addr) => addr,
226            Err(e) => {
227                log::warn!("Failed to get local address, defaulting to 0x20: {:?}", e);
228                Address(0x20)
229            }
230        };
231
232        Ok(Self {
233            inner,
234            recv_timeout,
235            seq: -1,
236            my_addr,
237        })
238    }
239
240    fn load_my_address_from_file(file: &mut std::fs::File) -> io::Result<Address> {
241        let mut my_addr: u32 = 8;
242        unsafe { ioctl::ipmi_get_my_address(file.as_raw_fd(), std::ptr::addr_of_mut!(my_addr))? };
243        if let Ok(addr) = u8::try_from(my_addr) {
244            Ok(Address(addr))
245        } else {
246            Err(io::Error::other(format!(
247                "ipmi_get_my_address returned non-u8 address: {my_addr}"
248            )))
249        }
250    }
251}
252
253impl IpmiConnection for File {
254    type SendError = io::Error;
255    type RecvError = io::Error;
256    type Error = io::Error;
257
258    fn send(&mut self, request: &mut Request) -> io::Result<()> {
259        let mut addr: IpmiAddr = match request.target() {
260            RequestTargetAddress::BmcOrIpmb(a, _, lun) if a == self.my_addr => {
261                RequestTargetAddress::Bmc(lun)
262            }
263            x => x,
264        }
265        .into();
266
267        self.seq += 1;
268
269        let netfn = request.netfn_raw();
270        let cmd = request.cmd();
271        let seq = self.seq;
272        let data = request.data_mut();
273
274        let data_len = data.len() as u16;
275        let ptr = data.as_mut_ptr();
276
277        log::debug!("Sending request (netfn: 0x{netfn:02X}, cmd: 0x{cmd:02X}) to {addr}");
278        let ipmi_message = IpmiMessage {
279            netfn,
280            cmd,
281            data_len,
282            data: ptr,
283        };
284        let mut request = IpmiRequest {
285            addr: addr.ptr(),
286            addr_len: addr.size(),
287            msg_id: seq,
288            message: ipmi_message,
289        };
290
291        request.log(log::Level::Trace);
292
293        // SAFETY: we send a mut pointer to an owned struct (`request`),
294        // which has the correct layout for this IOCTL call.
295        unsafe {
296            ioctl::ipmi_send_request(self.fd(), std::ptr::addr_of_mut!(request))?;
297        }
298
299        // Ensure that data and bmc_addr live until _after_ the IOCTL completes.
300        #[allow(clippy::drop_non_drop)]
301        drop(request);
302        #[allow(clippy::drop_non_drop)]
303        drop(addr);
304
305        Ok(())
306    }
307
308    fn recv(&mut self) -> io::Result<Response> {
309        let start = std::time::Instant::now();
310
311        let mut bmc_addr = IpmiSysIfaceAddr::bmc(0);
312
313        let mut response_data = [0u8; 1024];
314
315        let response_data_len = response_data.len() as u16;
316        let response_data_ptr = response_data.as_mut_ptr();
317
318        let mut recv = IpmiRecv {
319            addr: std::ptr::addr_of_mut!(bmc_addr) as *mut u8,
320            addr_len: core::mem::size_of::<IpmiSysIfaceAddr>() as u32,
321            msg_id: 0,
322            recv_type: 0,
323            message: IpmiMessage {
324                netfn: 0,
325                cmd: 0,
326                data_len: response_data_len,
327                data: response_data_ptr,
328            },
329        };
330
331        // Poll the device for available data.
332        //
333        // As of 2026-01-14, the linux driver tracks state for our
334        // `fd`, so any data received here will be in response to
335        // command we have sent.
336        //
337        // Ref: https://github.com/datdenkikniet/ipmi-rs/issues/39#issuecomment-3747421945
338        let mut polls = [PollFd::new(self.inner.as_fd(), PollFlags::POLLIN)];
339        let poll = nix::poll::poll(
340            polls.as_mut_slice(),
341            self.recv_timeout.as_millis().try_into().unwrap_or(u16::MAX),
342        )?;
343
344        if poll != 1 {
345            log::warn!(
346                "Failed to receive message after waiting for {} ms.",
347                start.elapsed().as_millis(),
348            );
349
350            return Err(Errno::EAGAIN.into());
351        }
352
353        // SAFETY: we send a mut pointer to a fully owned struct (`recv`),
354        // which has the correct layout for this IOCTL call.
355        let ipmi_result =
356            unsafe { ioctl::ipmi_recv_msg_trunc(self.fd(), std::ptr::addr_of_mut!(recv)) };
357
358        let ipmi_result = match ipmi_result {
359            Ok(_) => recv,
360            Err(e) => {
361                log::error!("Error occurred while reading from IPMI: {e:?}");
362                return Err(e.into());
363            }
364        };
365
366        // Ensure that response_data and bmc_addr live until _after_ the
367        // IOCTL completes.
368        #[allow(dropping_copy_types)]
369        drop(response_data);
370        #[allow(clippy::drop_non_drop)]
371        drop(bmc_addr);
372
373        log::debug!("Received response after {} ms", start.elapsed().as_millis());
374        ipmi_result.log(log::Level::Trace);
375
376        match Response::try_from(ipmi_result) {
377            Ok(response) => {
378                if response.seq() == self.seq {
379                    Ok(response)
380                } else {
381                    Err(io::Error::other(format!(
382                        "Invalid sequence number on response. Expected {}, got {}",
383                        self.seq,
384                        response.seq()
385                    )))
386                }
387            }
388            Err(e) => Err(io::Error::other(format!(
389                "Error while creating response. {e:?}"
390            ))),
391        }
392    }
393
394    fn send_recv(&mut self, request: &mut Request) -> io::Result<Response> {
395        self.send(request)?;
396
397        self.recv()
398    }
399}