ipmi_rs/
file.rs

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