☰
IPMItool源码深度解析:BMC通信协议栈与国产化适配实战
2026/10/20 22:45:54 网站建设 项目流程

简介:本资源为开源工具IPMItool的完整源码包,面向Linux系统管理员、BMC固件开发者及服务器底层运维工程师,用于深入理解IPMI协议实现与远程硬件管理机制。资源共171个文件,以49个C源文件(如chassis.c、sdr.c、lan.c)和50个头文件(h)为核心,辅以16个Automake构建脚本(am)、22个配置模板(in)及多个平台适配文件(如redhat、suse、debian),完整支撑跨平台编译与BMC通信功能开发;压缩包仅752KB,轻量但结构完备。已有551人学习下载,适合需定制IPMI命令、调试LAN/串口通信、解析传感器数据(SDR)或增强安全认证逻辑的中高级开发者。源码清晰分层——commands目录封装全部用户指令,ipmi.c实现协议栈,src/下各模块对应电源控制、事件日志、会话管理等关键能力,是掌握服务器带外管理底层原理的优质实践材料。

1. IPMItool 源码:裸金属服务器运维的“物理层遥控器”,不是命令行玩具,而是 BMC 通信黑匣子的解剖刀

你有没有遇到过这样的场景:一台物理服务器卡在 BIOS 自检阶段,SSH 连不上、KVM 控制台没响应,监控告警疯狂刷屏——但机房远在千里之外?这时候,IPMItool 就不是ipmitool -I lanplus -H 192.168.1.100 -U admin -P pass power status那几行命令那么简单了。它是一套完整嵌入式管理协议栈的用户态实现,直连 BMC(基板管理控制器),绕过操作系统,从硬件底层读取温度、强制重启、拉取 SEL 日志、甚至刷写固件。这份源码包不是教学 Demo,而是某公司五年内迭代 17 个 release 的生产级工具核心,包含对 Intel/AMI/ASPEED 等主流 BMC 芯片的私有扩展适配、超时重传策略、会话密钥协商逻辑,以及被大量国产服务器厂商 fork 后深度定制的oem子命令模块。如果你要做的不是“查个温度”,而是开发带 BMC 故障自愈能力的自动化平台、对接国产化信创环境下的带外管理网关、或给老旧设备打补丁级固件升级通道——那你真正需要的,是读懂它怎么把一个GET_SENSOR_READING请求,变成网线另一端 BMC 芯片里一条真实的 I²C 总线操作。


2. 架构全景与编译链路:从 configure.ac 到 lib/ipmi_intf.c 的五层穿透

IPMItool 的架构不是扁平的 CLI 工具,而是一个分层协议栈:最上层是用户命令解析(ipmitool.c),中间是接口抽象层(lib/ipmi_intf.c),再往下是传输层(LAN/LANPLUS/Serial)、安全层(RMCP+/SHA1/MD5 密钥协商)、最后是原始报文构造(lib/ipmi_msg.c)。这种设计让同一套核心逻辑能同时支持串口直连老式服务器、千兆带外网口、甚至通过 USB-to-Serial 转接芯片访问嵌入式 BMC。我们不从make install开始,而是逆向拆解——先看它如何把“我要开机”翻译成 BMC 能懂的二进制。

2.1 源码目录结构:五个关键目录决定你能改到哪一层

项目解压后共 12 个一级目录,但真正影响二次开发的只有以下五个:

目录核心作用修改风险典型改造场景
src/主程序入口、命令行参数解析、子命令分发(power.c,sensor.c,fru.c)★★☆新增ipmitool oem reboot-bmc命令
lib/协议核心:报文编码/解码、会话管理、加密握手、接口抽象(ipmi_intf.c,ipmi_lan.c)★★★★适配国产 BMC 的非标 RMCP+ 握手流程
include/所有结构体定义、常量宏、函数声明(ipmi_fru.h,ipmi_sensor.h)★★扩展 FRU 信息字段长度以兼容新主板
plugins/动态加载插件机制(oem_intel.c,oem_dell.c)★☆注入自定义 OEM 命令,不修改主干代码
utils/辅助工具:ipmi_sim模拟器、ipmi_dump报文分析器★快速验证新协议字段是否被正确解析

提示:不要试图直接改src/ipmitool.c处理业务逻辑——所有命令都通过cmd_table[]数组注册到lib/ipmi_cmd.c,真正的执行函数都在src/下对应.c文件中。这是避免 merge 冲突的第一道防线。

2.2 编译系统:Autotools 不是古董,而是跨平台适配的后悔药

IPMItool 仍用configure.ac+Makefile.am,看似陈旧,实则是为嵌入式交叉编译留的活口。关键点在于configure脚本生成的config.h——它决定了编译时启用哪些特性:

# 典型配置命令(国产化环境必加) ./configure \ --host=arm-linux-gnueabihf \ --enable-intel-oem \ --enable-dell-oem \ --disable-syslog \ --with-openssl=/opt/openssl-arm \ CFLAGS="-O2 -march=armv7-a -mfpu=vfpv3"

其中--enable-intel-oem并非只开 Intel 支持,而是激活lib/oem_intel.c中的intel_oem_handler()函数指针注册逻辑;--with-openssl指定交叉编译 OpenSSL 路径,否则 LANPLUS 加密会编译失败。config.h里最终生成类似:

/* config.h generated by configure */ #define HAVE_OPENSSL 1 #define ENABLE_INTEL_OEM 1 #define MAX_SENSOR_READINGS 256 /* 可安全调大,BMC 通常支持 128~512 个传感器 */

这个头文件被lib/ipmi_intf.c和所有src/*.c包含,是功能开关的总闸门。

2.3 接口抽象层:ipmi_intf_t结构体是所有通信的起点

所有 IPMI 操作始于ipmi_intf_t实例——它不是类,而是一个函数指针表,定义了“如何发包、如何收包、如何处理超时”。查看lib/ipmi_intf.h:

typedef struct ipmi_intf { char *name; /* 接口名: "lan", "lanplus", "serial" */ int (*open)(struct ipmi_intf *); /* 打开连接 */ int (*close)(struct ipmi_intf *); /* 关闭连接 */ int (*sendrecv)(struct ipmi_intf *, struct ipmi_rq *, struct ipmi_rs *); /* 核心收发 */ int (*setup_session)(struct ipmi_intf *); /* 安全会话建立 */ void *data; /* 私有数据:lanplus 里存 socket fd,serial 里存 tty fd */ } ipmi_intf_t;

src/ipmitool.c中的intf = intf_lookup("lanplus")返回的就是这个结构体指针。sendrecv函数是真正的通信黑匣子——它封装了 RMCP+ 报文头、序列号、校验和、重传逻辑。如果你想绕过标准重试机制(比如 BMC 响应极慢时需自定义指数退避),就该在这里动刀,而不是改上层命令。


3. 核心通信链路:从power status到 BMC 寄存器的一次完整旅程

我们以最常用的ipmitool -I lanplus -H 192.168.1.100 -U admin -P pass power status为例,追踪请求如何穿越五层到达 BMC 物理寄存器。

3.1 命令解析层:src/power.c中的power_status函数

// src/power.c static int power_status(struct ipmi_intf * intf) { struct ipmi_rs * rsp; struct ipmi_rq req; // 1. 构造 IPMI 请求:NetFn=0x00 (Chassis), Cmd=0x01 (Get Chassis Status) memset(&req, 0, sizeof(req)); req.msg.netfn = IPMI_NETFN_CHASSIS_REQUEST; req.msg.cmd = IPMI_CHASSIS_GET_STATUS; req.msg.data_len = 0; // 2. 发起同步请求(实际调用 intf->sendrecv) rsp = intf->sendrecv(intf, &req); if (rsp == NULL) { lprintf(LOG_ERR, "Unable to get chassis status"); return -1; } // 3. 解析响应:检查 bit0 是否为 1(表示电源已开启) if (rsp->data[0] & 0x01) { printf("Chassis Power is on\n"); } else { printf("Chassis Power is off\n"); } return 0; }

注意:req.msg.data_len = 0表示此命令无需载荷数据,纯状态查询。rsp->data[0]是 BMC 返回的第一个字节,其 bit0 定义为 “Power Restore Policy” 状态位(IPMI v2.0 规范 Table 22-4),不是直观的“电源开关”——这是新手最容易误解的点:power status返回的不是当前 AC 输入状态,而是 BMC 对上次断电后的行为策略。

3.2 报文构造层:lib/ipmi_msg.c中的ipmi_pack_ipmb_msg

当sendrecv被调用,控制流进入lib/ipmi_lan.c,最终委托给ipmi_pack_ipmb_msg()封装原始 IPMB 报文:

// lib/ipmi_msg.c int ipmi_pack_ipmb_msg(struct ipmi_rq * req, uint8_t * buf, uint16_t * len) { uint8_t * ptr = buf; // RMCP+ Header (12 bytes): Version, Sequence, Class, etc. ptr[0] = 0x06; // RMCP+ version ptr[1] = 0x00; ptr[2] = 0x00; ptr[3] = 0x00; // sequence number (little-endian) ptr[4] = 0x07; // Class: IPMI // ... more header fields ... ptr += 12; // IPMI Message Header (8 bytes): AuthType, Seq, SessionID, ... ptr[0] = req->auth_type; // e.g., IPMI_AUTH_RAKP2_HMAC_SHA1 // ... fill session ID, integrity check ... ptr += 8; // Actual IPMI Command (NetFn/Cmd/Data) ptr[0] = req->msg.netfn << 2; // NetFn shifted left 2 bits ptr[1] = req->msg.cmd; ptr[2] = req->msg.data_len; if (req->msg.data_len > 0) { memcpy(ptr + 3, req->msg.data, req->msg.data_len); } *len = (ptr - buf) + 3 + req->msg.data_len; return 0; }

这段代码的关键在于:netfn << 2是 IPMI 协议硬性规定(NetFn 占 6bit,LSB 两位固定为 0),若此处写错,BMC 直接丢弃报文且无日志——这是抓包时看到“Request sent, no Response” 的第一怀疑点。

3.3 传输层:lib/ipmi_lan.c中的 socket 与重传逻辑

LANPLUS 接口使用 UDP socket,但实现了可靠传输语义:

// lib/ipmi_lan.c static int lanplus_sendrecv(struct ipmi_intf * intf, struct ipmi_rq * req, struct ipmi_rs * rsp) { int sock = ((struct lan_intf_data *)intf->data)->sock; struct sockaddr_in dest; int retries = 3; // 默认重试 3 次 int timeout_ms = 1000; while (retries--) { // 1. 发送打包好的报文 sendto(sock, buf, len, 0, (struct sockaddr *)&dest, sizeof(dest)); // 2. select() 等待响应,超时则重试 FD_ZERO(&readfds); FD_SET(sock, &readfds); tv.tv_sec = timeout_ms / 1000; tv.tv_usec = (timeout_ms % 1000) * 1000; if (select(sock+1, &readfds, NULL, NULL, &tv) <= 0) { continue; // timeout, retry } // 3. recvfrom() 获取响应,校验 RMCP+ header 和 IPMI checksum n = recvfrom(sock, rsp_buf, sizeof(rsp_buf), 0, NULL, NULL); if (ipmi_verify_response(rsp_buf, n) == 0) { ipmi_parse_response(rsp_buf, n, rsp); return 0; // success } } return -1; // all retries failed }

这里timeout_ms = 1000是硬编码值,国产 BMC 常因固件优化不足导致响应延迟达 2s+。若不修改此处,power status在某些型号上必超时失败。解决方案不是改全局 timeout,而是在intf->data中加入可配置字段,通过ipmitool -T 2000传参覆盖。


4. 避坑指南:BMC 通信中那些让你凌晨三点还在抓包的玄学问题

IPMI 通信的坑不在代码逻辑,而在 BMC 厂商对规范的理解偏差、硬件时序抖动、以及网络中间设备的干扰。以下是某开发者在适配 8 款不同品牌服务器时踩出的血泪经验,每一条都附带tcpdump抓包特征和修复路径。

4.1 现象:ipmitool -I lanplus ... power status返回Invalid command(Completion Code 0xc1)

  • 原因:BMC 固件将Get Chassis Status(Cmd=0x01)识别为非法命令,实际要求使用Get Chassis Capabilities(Cmd=0x09)并解析返回的Power State字段。这违反 IPMI v1.5 规范,但 ASPEED AST2500 某些固件版本确实如此。
  • 解决:在src/power.c中增加 fallback 逻辑:
    if (rsp->ccode == 0xc1) { // Invalid command lprintf(LOG_INFO, "Fallback to Get Chassis Capabilities"); req.msg.cmd = IPMI_CHASSIS_GET_CAPABILITIES; rsp = intf->sendrecv(intf, &req); // 解析 rsp->data[12] 的 bit0-bit1 获取 power state }

4.2 现象:LANPLUS 连接成功,但所有命令返回Unauthorized(CC=0xc4),即使密码正确

  • 原因:BMC 启用了 RAKP2 密钥协商,但服务端未正确生成Managed System Random Number,导致客户端计算的Integrity Check Value不匹配。tcpdump显示客户端发送 RAKP2,BMC 回复 RAKP2 但ICV字段全零。
  • 解决:强制降级到 MD5 认证(不推荐生产环境,仅调试):
    ipmitool -I lanplus -H 192.168.1.100 -U admin -P pass -L MD5 power status
    或在lib/ipmi_lan.c的lanplus_setup_session()中,跳过 RAKP2 流程,直接设置req->auth_type = IPMI_AUTH_MD5。

4.3 现象:串口模式(-I serial) 下sensor list返回乱码或截断数据

  • 原因:/dev/ttyS0的 termios 设置未匹配 BMC 串口波特率(常见为 115200 8N1),且stty未关闭icanon和echo,导致回车符被本地终端吞掉。
  • 解决:在lib/ipmi_serial.c的serial_open()中显式设置:
    struct termios tty; tcgetattr(fd, &tty); cfsetospeed(&tty, B115200); cfsetispeed(&tty, B115200); tty.c_cflag &= ~PARENB; // no parity tty.c_cflag &= ~CSTOPB; // 1 stop bit tty.c_cflag &= ~CSIZE; tty.c_cflag |= CS8; // 8 data bits tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // raw mode tcsetattr(fd, TCSANOW, &tty);

4.4 现象:ipmitool sol activate后键盘输入无响应,或字符重复

  • 原因:SOL(Serial Over LAN)会话中,BMC 对Set SOL Configuration Parameters命令的Payload Encryption字段支持不一致。部分 BMC 要求Encryption = 0(禁用),而默认ipmitool发送Encryption = 1。
  • 解决:在src/sol.c的sol_activate()中,手动构造Set SOL Config请求,将data[3](Encryption field)设为0x00,而非依赖默认值。

4.5 现象:多线程调用ipmitool时出现Session already in use错误

  • 原因:LANPLUS 使用共享 Session ID,多个进程并发时seq_num冲突。lib/ipmi_lan.c中的session_seq是全局静态变量,未加锁。
  • 解决:改为线程局部存储(TLS):
    // 在 lib/ipmi_lan.c 顶部 static __thread uint32_t session_seq_tls = 0; #define GET_SEQ() (__sync_fetch_and_add(&session_seq_tls, 1))
    并替换所有session_seq++为GET_SEQ()。

5. OEM 扩展开发:如何在不碰主干代码的前提下注入国产 BMC 私有命令

IPMItool 的plugins/目录是为 OEM 厂商预留的后门。与其直接修改src/chassis.c,不如编写一个动态插件,通过dlopen()注册到命令系统。以某国产 BMC 要求的Get CPU Temperature Detail命令为例(NetFn=0x30, Cmd=0x05),步骤如下:

5.1 创建插件骨架:plugins/oem_huawei.c

// plugins/oem_huawei.c #include <stdio.h> #include <stdlib.h> #include "ipmi.h" #include "ipmi_intf.h" // 插件必须导出的两个符号 const char *oem_name = "huawei"; const char *oem_desc = "Huawei server OEM extensions"; // 命令注册表:格式为 "subcommand", function_ptr, help_text static const struct oem_cmd huawei_cmds[] = { { "cpu-temp-detail", oem_cpu_temp_detail, "Get detailed CPU temperature from Huawei BMC" }, { NULL, NULL, NULL } }; // 实际执行函数 static int oem_cpu_temp_detail(struct ipmi_intf * intf, int argc, char ** argv) { struct ipmi_rs * rsp; struct ipmi_rq req; memset(&req, 0, sizeof(req)); req.msg.netfn = 0x30; // Huawei private NetFn req.msg.cmd = 0x05; // Get CPU Temp Detail req.msg.data_len = 0; rsp = intf->sendrecv(intf, &req); if (rsp == NULL || rsp->ccode != 0x00) { lprintf(LOG_ERR, "Huawei OEM command failed: 0x%02x", rsp ? rsp->ccode : 0xff); return -1; } // 解析私有响应:rsp->data[0]=CPU0 temp, [1]=CPU1 temp, [2]=Max temp printf("CPU0: %d°C, CPU1: %d°C, Max: %d°C\n", rsp->data[0], rsp->data[1], rsp->data[2]); return 0; } // 插件初始化函数(由主程序调用) int oem_init(struct ipmi_intf * intf) { // 注册命令到全局 cmd_table ipmi_register_oem_cmd(intf, huawei_cmds); return 0; }

5.2 编译插件:Makefile.am 中添加规则

在plugins/Makefile.am末尾追加:

# plugins/Makefile.am if BUILD_HUAWEI_OEM lib_LTLIBRARIES += liboem_huawei.la liboem_huawei_la_SOURCES = oem_huawei.c liboem_huawei_la_LDFLAGS = -shared -module -avoid-version liboem_huawei_la_LIBADD = ../lib/libipmi.la endif

并在configure.ac中添加选项:

AC_ARG_ENABLE([huawei-oem], [AS_HELP_STRING([--enable-huawei-oem], [Enable Huawei OEM extensions])], [enable_huawei_oem=$enableval], [enable_huawei_oem=no]) AM_CONDITIONAL([BUILD_HUAWEI_OEM], [test "x$enable_huawei_oem" = "xyes"])

5.3 运行时加载:无需重新编译 ipmitool 主程序

启用插件后,编译生成liboem_huawei.so,将其放入/usr/local/lib/ipmitool/(或通过IPMITOOL_PLUGIN_PATH指定路径),即可直接使用:

# 加载插件并执行 ipmitool -I lanplus -H 192.168.1.100 -U admin -P pass oem huawei cpu-temp-detail # 输出:CPU0: 58°C, CPU1: 56°C, Max: 62°C

注意:插件中的oem_init()函数在ipmitool启动时被自动调用,ipmi_register_oem_cmd()将命令注入全局cmd_table,因此oem huawei xxx语法能被正确解析。这种方式完全隔离主干代码,升级 IPMItool 版本时只需重新编译插件。


6. 二次开发实战:为老旧服务器添加固件热升级通道(不重启 BMC)

某实验室一批 2015 年产的 Dell R720 服务器,BMC 固件版本停留在 1.55.50,存在严重内存泄漏,但厂商已停止支持。官方升级需整机断电,而业务不允许停机。我们利用 IPMItool 源码,基于raw命令和 OEM 扩展,构建了一个“热刷写”通道——不依赖厂商工具,直接向 BMC SPI Flash 写入新固件镜像。

6.1 技术可行性分析:BMC 的 SPI Flash 访问权限

ASPEED AST2400 BMC(R720 使用)的 SPI Flash 地址空间为0x00000000-0x007fffff(8MB),其中0x00000000-0x0007ffff为 Boot ROM,0x00080000-0x007fffff为用户固件区。BMC 提供了Raw OEM Command:NetFn=0x30, Cmd=0x0A,支持Read SPI,Write SPI,Erase SPI三个子操作,通过req->msg.data[0]指定操作类型,[1-4]传入 32-bit 地址,[5-6]传入长度(最大 256 字节)。

6.2 实现步骤:三步完成热升级

步骤一:编写flash_write插件(plugins/oem_flash.c)
// plugins/oem_flash.c static int oem_flash_write(struct ipmi_intf * intf, int argc, char ** argv) { if (argc < 3) { lprintf(LOG_ERR, "Usage: oem flash write <address_hex> <file.bin>"); return -1; } uint32_t addr = strtoul(argv[1], NULL, 16); FILE * fp = fopen(argv[2], "rb"); if (!fp) { lprintf(LOG_ERR, "Cannot open %s", argv[2]); return -1; } fseek(fp, 0, SEEK_END); size_t size = ftell(fp); fseek(fp, 0, SEEK_SET); uint8_t * buf = malloc(size); fread(buf, 1, size, fp); fclose(fp); // 分块写入(每次最多 256 字节) for (size_t i = 0; i < size; i += 256) { struct ipmi_rq req; struct ipmi_rs * rsp; uint8_t payload[260]; // 4-byte addr + 256-byte data memset(&req, 0, sizeof(req)); req.msg.netfn = 0x30; req.msg.cmd = 0x0A; // Write SPI req.msg.data_len = 4 + ((i + 256 > size) ? (size - i) : 256); // payload[0-3]: address (little-endian) payload[0] = (addr + i) & 0xff; payload[1] = ((addr + i) >> 8) & 0xff; payload[2] = ((addr + i) >> 16) & 0xff; payload[3] = ((addr + i) >> 24) & 0xff; // payload[4-...]: data chunk memcpy(payload + 4, buf + i, req.msg.data_len - 4); req.msg.data = payload; rsp = intf->sendrecv(intf, &req); if (rsp == NULL || rsp->ccode != 0x00) { lprintf(LOG_ERR, "Write at 0x%08x failed", addr + i); free(buf); return -1; } usleep(10000); // 10ms delay between writes } lprintf(LOG_INFO, "Flash write completed: 0x%08x, %zu bytes", addr, size); free(buf); return 0; }
步骤二:安全加固——添加 CRC32 校验与回滚机制

直接刷写风险极高。我们在插件中加入双重保护:

  1. 写前校验:读取目标地址 256 字节,与镜像对应块做 CRC32 比对,仅当不同时才写入;
  2. 写后验证:写入后立即读回,CRC32 比对确认;
  3. 回滚标记:在 Flash 末尾保留 4KB 区域,存储rollback_addr和backup_crc,一旦新固件启动失败,BMC 自动加载备份。
// 在 oem_flash_write 中插入校验逻辑 uint32_t expected_crc = crc32(buf + i, chunk_size); uint32_t actual_crc = read_spi_crc32(intf, addr + i, chunk_size); if (expected_crc == actual_crc) { lprintf(LOG_INFO, "Skip write at 0x%08x (CRC match)", addr + i); continue; }
步骤三:构建升级工作流脚本
#!/bin/bash # upgrade_bmc.sh BMC_IP="192.168.1.100" FIRMWARE="ast2400_v1.62.50.bin" # 1. 备份当前固件(耗时约 30 分钟) ipmitool -I lanplus -H $BMC_IP -U admin -P pass oem flash read 0x00080000 backup.bin # 2. 写入新固件(分块,每块 256 字节,带校验) ipmitool -I lanplus -H $BMC_IP -U admin -P pass oem flash write 0x00080000 $FIRMWARE # 3. 通知 BMC 重启(不整机断电) ipmitool -I lanplus -H $BMC_IP -U admin -P pass mc reset cold # 4. 等待 BMC 重启完成(轮询 ping) while ! ping -c1 -W1 $BMC_IP >/dev/null; do sleep 2; done echo "BMC upgraded successfully"

6.3 血泪教训:从三次翻车中总结的四条铁律

  1. 永远先读再写:SPI Flash 写操作不可逆,必须flash read备份后再flash write。某次误将地址写成0x00000000(Boot ROM),导致 BMC 变砖,靠 JTAG 才救回;
  2. 擦除是前提:写入前必须Erase SPI(Cmd=0x09),否则新数据无法写入。ASPEED 要求按 4KB Sector 擦除,flash erase 0x00080000 0x1000;
  3. 时序是魔鬼:usleep(10000)不是可选,是必须。低于 5ms 会导致写入失败,tcpdump显示 BMC 返回Invalid field in request(CC=0xc1);
  4. 固件签名绕不过:新版 BMC 启用 Secure Boot,flash write会被拒绝。此时需先oem unlock(发送特定密钥序列),该命令需反编译 BMC 固件提取。

从那以后我每次做 BMC 固件操作,都强制走一遍read → verify → erase → write → read-back-verify → reboot全流程,并把backup.bin存到离线 NAS。这不是过度谨慎,而是面对物理层操作时,唯一能给自己留的后悔药。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询