1 #pragma once 2 3 #include "app/channel.hpp" 4 #include "user_channel/cipher_mgmt.hpp" 5 6 #include <arpa/inet.h> 7 #include <netinet/ether.h> 8 9 #include <array> 10 #include <bitset> 11 #include <cinttypes> 12 #include <cstdint> 13 #include <cstring> 14 #include <fstream> 15 #include <functional> 16 #include <ipmid/api-types.hpp> 17 #include <ipmid/api.hpp> 18 #include <ipmid/message.hpp> 19 #include <ipmid/message/types.hpp> 20 #include <ipmid/types.hpp> 21 #include <ipmid/utils.hpp> 22 #include <optional> 23 #include <phosphor-logging/elog-errors.hpp> 24 #include <phosphor-logging/elog.hpp> 25 #include <phosphor-logging/log.hpp> 26 #include <sdbusplus/bus.hpp> 27 #include <sdbusplus/exception.hpp> 28 #include <string> 29 #include <string_view> 30 #include <type_traits> 31 #include <unordered_map> 32 #include <unordered_set> 33 #include <user_channel/channel_layer.hpp> 34 #include <utility> 35 #include <vector> 36 #include <xyz/openbmc_project/Common/error.hpp> 37 #include <xyz/openbmc_project/Network/EthernetInterface/server.hpp> 38 #include <xyz/openbmc_project/Network/IP/server.hpp> 39 #include <xyz/openbmc_project/Network/Neighbor/server.hpp> 40 41 namespace ipmi 42 { 43 namespace transport 44 { 45 46 // D-Bus Network Daemon definitions 47 constexpr auto PATH_ROOT = "/xyz/openbmc_project/network"; 48 constexpr auto INTF_ETHERNET = "xyz.openbmc_project.Network.EthernetInterface"; 49 constexpr auto INTF_IP = "xyz.openbmc_project.Network.IP"; 50 constexpr auto INTF_IP_CREATE = "xyz.openbmc_project.Network.IP.Create"; 51 constexpr auto INTF_MAC = "xyz.openbmc_project.Network.MACAddress"; 52 constexpr auto INTF_NEIGHBOR = "xyz.openbmc_project.Network.Neighbor"; 53 constexpr auto INTF_NEIGHBOR_CREATE_STATIC = 54 "xyz.openbmc_project.Network.Neighbor.CreateStatic"; 55 constexpr auto INTF_VLAN = "xyz.openbmc_project.Network.VLAN"; 56 constexpr auto INTF_VLAN_CREATE = "xyz.openbmc_project.Network.VLAN.Create"; 57 58 /** @brief IPMI LAN Parameters */ 59 enum class LanParam : uint8_t 60 { 61 SetStatus = 0, 62 AuthSupport = 1, 63 AuthEnables = 2, 64 IP = 3, 65 IPSrc = 4, 66 MAC = 5, 67 SubnetMask = 6, 68 Gateway1 = 12, 69 Gateway1MAC = 13, 70 VLANId = 20, 71 CiphersuiteSupport = 22, 72 CiphersuiteEntries = 23, 73 cipherSuitePrivilegeLevels = 24, 74 IPFamilySupport = 50, 75 IPFamilyEnables = 51, 76 IPv6Status = 55, 77 IPv6StaticAddresses = 56, 78 IPv6DynamicAddresses = 59, 79 IPv6RouterControl = 64, 80 IPv6StaticRouter1IP = 65, 81 IPv6StaticRouter1MAC = 66, 82 IPv6StaticRouter1PrefixLength = 67, 83 IPv6StaticRouter1PrefixValue = 68, 84 }; 85 86 /** @brief IPMI IP Origin Types */ 87 enum class IPSrc : uint8_t 88 { 89 Unspecified = 0, 90 Static = 1, 91 DHCP = 2, 92 BIOS = 3, 93 BMC = 4, 94 }; 95 96 /** @brief IPMI Set Status */ 97 enum class SetStatus : uint8_t 98 { 99 Complete = 0, 100 InProgress = 1, 101 Commit = 2, 102 }; 103 104 /** @brief IPMI Family Suport Bits */ 105 namespace IPFamilySupportFlag 106 { 107 constexpr uint8_t IPv6Only = 0; 108 constexpr uint8_t DualStack = 1; 109 constexpr uint8_t IPv6Alerts = 2; 110 } // namespace IPFamilySupportFlag 111 112 /** @brief IPMI IPFamily Enables Flag */ 113 enum class IPFamilyEnables : uint8_t 114 { 115 IPv4Only = 0, 116 IPv6Only = 1, 117 DualStack = 2, 118 }; 119 120 /** @brief IPMI IPv6 Dyanmic Status Bits */ 121 namespace IPv6StatusFlag 122 { 123 constexpr uint8_t DHCP = 0; 124 constexpr uint8_t SLAAC = 1; 125 }; // namespace IPv6StatusFlag 126 127 /** @brief IPMI IPv6 Source */ 128 enum class IPv6Source : uint8_t 129 { 130 Static = 0, 131 SLAAC = 1, 132 DHCP = 2, 133 }; 134 135 /** @brief IPMI IPv6 Address Status */ 136 enum class IPv6AddressStatus : uint8_t 137 { 138 Active = 0, 139 Disabled = 1, 140 }; 141 142 namespace IPv6RouterControlFlag 143 { 144 constexpr uint8_t Static = 0; 145 constexpr uint8_t Dynamic = 1; 146 }; // namespace IPv6RouterControlFlag 147 148 // LAN Handler specific response codes 149 constexpr Cc ccParamNotSupported = 0x80; 150 constexpr Cc ccParamSetLocked = 0x81; 151 constexpr Cc ccParamReadOnly = 0x82; 152 153 // VLANs are a 12-bit value 154 constexpr uint16_t VLAN_VALUE_MASK = 0x0fff; 155 constexpr uint16_t VLAN_ENABLE_FLAG = 0x8000; 156 157 // Arbitrary v6 Address Limits to prevent too much output in ipmitool 158 constexpr uint8_t MAX_IPV6_STATIC_ADDRESSES = 15; 159 constexpr uint8_t MAX_IPV6_DYNAMIC_ADDRESSES = 15; 160 161 /** @brief The dbus parameters for the interface corresponding to a channel 162 * This helps reduce the number of mapper lookups we need for each 163 * query and simplifies finding the VLAN interface if needed. 164 */ 165 struct ChannelParams 166 { 167 /** @brief The channel ID */ 168 int id; 169 /** @brief channel name for the interface */ 170 std::string ifname; 171 /** @brief Name of the service on the bus */ 172 std::string service; 173 /** @brief Lower level adapter path that is guaranteed to not be a VLAN */ 174 std::string ifPath; 175 /** @brief Logical adapter path used for address assignment */ 176 std::string logicalPath; 177 }; 178 179 /** @brief A trivial helper used to determine if two PODs are equal 180 * 181 * @params[in] a - The first object to compare 182 * @params[in] b - The second object to compare 183 * @return True if the objects are the same bytewise 184 */ 185 template <typename T> 186 bool equal(const T& a, const T& b) 187 { 188 static_assert(std::is_trivially_copyable_v<T>); 189 return std::memcmp(&a, &b, sizeof(T)) == 0; 190 } 191 192 /** @brief Copies bytes from an array into a trivially copyable container 193 * 194 * @params[out] t - The container receiving the data 195 * @params[in] bytes - The data to copy 196 */ 197 template <size_t N, typename T> 198 void copyInto(T& t, const std::array<uint8_t, N>& bytes) 199 { 200 static_assert(std::is_trivially_copyable_v<T>); 201 static_assert(N == sizeof(T)); 202 std::memcpy(&t, bytes.data(), bytes.size()); 203 } 204 205 /** @brief Gets a generic view of the bytes in the input container 206 * 207 * @params[in] t - The data to reference 208 * @return A string_view referencing the bytes in the container 209 */ 210 template <typename T> 211 std::string_view dataRef(const T& t) 212 { 213 static_assert(std::is_trivially_copyable_v<T>); 214 return {reinterpret_cast<const char*>(&t), sizeof(T)}; 215 } 216 217 /** @brief Determines the ethernet interface name corresponding to a channel 218 * Tries to map a VLAN object first so that the address information 219 * is accurate. Otherwise it gets the standard ethernet interface. 220 * 221 * @param[in] bus - The bus object used for lookups 222 * @param[in] channel - The channel id corresponding to an ethernet interface 223 * @return Ethernet interface service and object path if it exists 224 */ 225 std::optional<ChannelParams> maybeGetChannelParams(sdbusplus::bus::bus& bus, 226 uint8_t channel); 227 228 /** @brief A trivial helper around maybeGetChannelParams() that throws an 229 * exception when it is unable to acquire parameters for the channel. 230 * 231 * @param[in] bus - The bus object used for lookups 232 * @param[in] channel - The channel id corresponding to an ethernet interface 233 * @return Ethernet interface service and object path 234 */ 235 ChannelParams getChannelParams(sdbusplus::bus::bus& bus, uint8_t channel); 236 237 /** @brief Trivializes using parameter getter functions by providing a bus 238 * and channel parameters automatically. 239 * 240 * @param[in] channel - The channel id corresponding to an ethernet interface 241 * ... 242 */ 243 template <auto func, typename... Args> 244 auto channelCall(uint8_t channel, Args&&... args) 245 { 246 sdbusplus::bus::bus bus(ipmid_get_sd_bus_connection()); 247 auto params = getChannelParams(bus, channel); 248 return std::invoke(func, bus, params, std::forward<Args>(args)...); 249 } 250 251 /** @brief Generic paramters for different address families */ 252 template <int family> 253 struct AddrFamily 254 { 255 }; 256 257 /** @brief Parameter specialization for IPv4 */ 258 template <> 259 struct AddrFamily<AF_INET> 260 { 261 using addr = in_addr; 262 static constexpr auto protocol = 263 sdbusplus::xyz::openbmc_project::Network::server::IP::Protocol::IPv4; 264 static constexpr size_t maxStrLen = INET6_ADDRSTRLEN; 265 static constexpr uint8_t defaultPrefix = 32; 266 static constexpr char propertyGateway[] = "DefaultGateway"; 267 }; 268 269 /** @brief Parameter specialization for IPv6 */ 270 template <> 271 struct AddrFamily<AF_INET6> 272 { 273 using addr = in6_addr; 274 static constexpr auto protocol = 275 sdbusplus::xyz::openbmc_project::Network::server::IP::Protocol::IPv6; 276 static constexpr size_t maxStrLen = INET6_ADDRSTRLEN; 277 static constexpr uint8_t defaultPrefix = 128; 278 static constexpr char propertyGateway[] = "DefaultGateway6"; 279 }; 280 281 /** @brief Interface Neighbor configuration parameters */ 282 template <int family> 283 struct IfNeigh 284 { 285 std::string path; 286 typename AddrFamily<family>::addr ip; 287 ether_addr mac; 288 }; 289 290 /** @brief Interface IP Address configuration parameters */ 291 template <int family> 292 struct IfAddr 293 { 294 std::string path; 295 typename AddrFamily<family>::addr address; 296 sdbusplus::xyz::openbmc_project::Network::server::IP::AddressOrigin origin; 297 uint8_t prefix; 298 }; 299 300 /** @brief Valid address origins for IPv6 */ 301 static inline const std::unordered_set< 302 sdbusplus::xyz::openbmc_project::Network::server::IP::AddressOrigin> 303 originsV6Static = {sdbusplus::xyz::openbmc_project::Network::server::IP:: 304 AddressOrigin::Static}; 305 static inline const std::unordered_set< 306 sdbusplus::xyz::openbmc_project::Network::server::IP::AddressOrigin> 307 originsV6Dynamic = { 308 sdbusplus::xyz::openbmc_project::Network::server::IP::AddressOrigin:: 309 DHCP, 310 sdbusplus::xyz::openbmc_project::Network::server::IP::AddressOrigin:: 311 SLAAC, 312 }; 313 314 /** @brief A lazy lookup mechanism for iterating over object properties stored 315 * in DBus. This will only perform the object lookup when needed, and 316 * retains a cache of previous lookups to speed up future iterations. 317 */ 318 class ObjectLookupCache 319 { 320 public: 321 using PropertiesCache = std::unordered_map<std::string, PropertyMap>; 322 323 /** @brief Creates a new ObjectLookupCache for the interface on the bus 324 * NOTE: The inputs to this object must outlive the object since 325 * they are only referenced by it. 326 * 327 * @param[in] bus - The bus object used for lookups 328 * @param[in] params - The parameters for the channel 329 * @param[in] intf - The interface we are looking up 330 */ 331 ObjectLookupCache(sdbusplus::bus::bus& bus, const ChannelParams& params, 332 const char* intf) : 333 bus(bus), 334 params(params), intf(intf), 335 objs(getAllDbusObjects(bus, params.logicalPath, intf, "")) 336 { 337 } 338 339 class iterator : public ObjectTree::const_iterator 340 { 341 public: 342 using value_type = PropertiesCache::value_type; 343 344 iterator(ObjectTree::const_iterator it, ObjectLookupCache& container) : 345 ObjectTree::const_iterator(it), container(container), 346 ret(container.cache.end()) 347 { 348 } 349 value_type& operator*() 350 { 351 ret = container.get(ObjectTree::const_iterator::operator*().first); 352 return *ret; 353 } 354 value_type* operator->() 355 { 356 return &operator*(); 357 } 358 359 private: 360 ObjectLookupCache& container; 361 PropertiesCache::iterator ret; 362 }; 363 364 iterator begin() noexcept 365 { 366 return iterator(objs.begin(), *this); 367 } 368 369 iterator end() noexcept 370 { 371 return iterator(objs.end(), *this); 372 } 373 374 private: 375 sdbusplus::bus::bus& bus; 376 const ChannelParams& params; 377 const char* const intf; 378 const ObjectTree objs; 379 PropertiesCache cache; 380 381 /** @brief Gets a cached copy of the object properties if possible 382 * Otherwise performs a query on DBus to look them up 383 * 384 * @param[in] path - The object path to lookup 385 * @return An iterator for the specified object path + properties 386 */ 387 PropertiesCache::iterator get(const std::string& path) 388 { 389 auto it = cache.find(path); 390 if (it != cache.end()) 391 { 392 return it; 393 } 394 auto properties = getAllDbusProperties(bus, params.service, path, intf); 395 return cache.insert({path, std::move(properties)}).first; 396 } 397 }; 398 399 /** @brief Turns an IP address string into the network byte order form 400 * NOTE: This version strictly validates family matches 401 * 402 * @param[in] address - The string form of the address 403 * @return A network byte order address or none if conversion failed 404 */ 405 template <int family> 406 std::optional<typename AddrFamily<family>::addr> 407 maybeStringToAddr(const char* address) 408 { 409 typename AddrFamily<family>::addr ret; 410 if (inet_pton(family, address, &ret) == 1) 411 { 412 return ret; 413 } 414 return std::nullopt; 415 } 416 417 /** @brief Turns an IP address string into the network byte order form 418 * NOTE: This version strictly validates family matches 419 * 420 * @param[in] address - The string form of the address 421 * @return A network byte order address 422 */ 423 template <int family> 424 typename AddrFamily<family>::addr stringToAddr(const char* address) 425 { 426 auto ret = maybeStringToAddr<family>(address); 427 if (!ret) 428 { 429 phosphor::logging::log<phosphor::logging::level::ERR>( 430 "Failed to convert IP Address", 431 phosphor::logging::entry("FAMILY=%d", family), 432 phosphor::logging::entry("ADDRESS=%s", address)); 433 phosphor::logging::elog< 434 sdbusplus::xyz::openbmc_project::Common::Error::InternalFailure>(); 435 } 436 return *ret; 437 } 438 439 /** @brief Turns an IP address in network byte order into a string 440 * 441 * @param[in] address - The string form of the address 442 * @return A network byte order address 443 */ 444 template <int family> 445 std::string addrToString(const typename AddrFamily<family>::addr& address) 446 { 447 std::string ret(AddrFamily<family>::maxStrLen, '\0'); 448 inet_ntop(family, &address, ret.data(), ret.size()); 449 ret.resize(strlen(ret.c_str())); 450 return ret; 451 } 452 453 /** @brief Converts a human readable MAC string into MAC bytes 454 * 455 * @param[in] mac - The MAC string 456 * @return MAC in bytes 457 */ 458 ether_addr stringToMAC(const char* mac); 459 /** @brief Searches the ip object lookup cache for an address matching 460 * the input parameters. NOTE: The index lacks stability across address 461 * changes since the network daemon has no notion of stable indicies. 462 * 463 * @param[in] bus - The bus object used for lookups 464 * @param[in] params - The parameters for the channel 465 * @param[in] idx - The index of the desired address on the interface 466 * @param[in] origins - The allowed origins for the address objects 467 * @param[in] ips - The object lookup cache holding all of the address info 468 * @return The address and prefix if it was found 469 */ 470 template <int family> 471 std::optional<IfAddr<family>> findIfAddr( 472 sdbusplus::bus::bus& bus, const ChannelParams& params, uint8_t idx, 473 const std::unordered_set< 474 sdbusplus::xyz::openbmc_project::Network::server::IP::AddressOrigin>& 475 origins, 476 ObjectLookupCache& ips) 477 { 478 for (const auto& [path, properties] : ips) 479 { 480 const auto& addrStr = std::get<std::string>(properties.at("Address")); 481 auto addr = maybeStringToAddr<family>(addrStr.c_str()); 482 if (!addr) 483 { 484 continue; 485 } 486 487 sdbusplus::xyz::openbmc_project::Network::server::IP::AddressOrigin 488 origin = sdbusplus::xyz::openbmc_project::Network::server::IP:: 489 convertAddressOriginFromString( 490 std::get<std::string>(properties.at("Origin"))); 491 if (origins.find(origin) == origins.end()) 492 { 493 continue; 494 } 495 496 if (idx > 0) 497 { 498 idx--; 499 continue; 500 } 501 502 IfAddr<family> ifaddr; 503 ifaddr.path = path; 504 ifaddr.address = *addr; 505 ifaddr.prefix = std::get<uint8_t>(properties.at("PrefixLength")); 506 ifaddr.origin = origin; 507 return std::move(ifaddr); 508 } 509 510 return std::nullopt; 511 } 512 /** @brief Trivial helper around findIfAddr that simplifies calls 513 * for one off lookups. Don't use this if you intend to do multiple 514 * lookups at a time. 515 * 516 * @param[in] bus - The bus object used for lookups 517 * @param[in] params - The parameters for the channel 518 * @param[in] idx - The index of the desired address on the interface 519 * @param[in] origins - The allowed origins for the address objects 520 * @return The address and prefix if it was found 521 */ 522 template <int family> 523 auto getIfAddr( 524 sdbusplus::bus::bus& bus, const ChannelParams& params, uint8_t idx, 525 const std::unordered_set< 526 sdbusplus::xyz::openbmc_project::Network::server::IP::AddressOrigin>& 527 origins) 528 { 529 ObjectLookupCache ips(bus, params, INTF_IP); 530 return findIfAddr<family>(bus, params, idx, origins, ips); 531 } 532 533 /** @brief Determines if the ethernet interface is using DHCP 534 * 535 * @param[in] bus - The bus object used for lookups 536 * @param[in] params - The parameters for the channel 537 * @return DHCPConf enumeration 538 */ 539 sdbusplus::xyz::openbmc_project::Network::server::EthernetInterface::DHCPConf 540 getDHCPProperty(sdbusplus::bus::bus& bus, const ChannelParams& params); 541 542 /** @brief Sets the DHCP v6 state on the given interface 543 * 544 * @param[in] bus - The bus object used for lookups 545 * @param[in] params - The parameters for the channel 546 * @param[in] requestedDhcp - DHCP state to assign (none, v6, both) 547 * @param[in] defaultMode - True: Use algorithmic assignment 548 * False: requestedDhcp assigned unconditionally 549 */ 550 void setDHCPv6Property(sdbusplus::bus::bus& bus, const ChannelParams& params, 551 const sdbusplus::xyz::openbmc_project::Network::server:: 552 EthernetInterface::DHCPConf requestedDhcp, 553 const bool defaultMode); 554 555 /** @brief Reconfigures the IPv6 address info configured for the interface 556 * 557 * @param[in] bus - The bus object used for lookups 558 * @param[in] params - The parameters for the channel 559 * @param[in] idx - The address index to operate on 560 * @param[in] address - The new address 561 * @param[in] prefix - The new address prefix 562 */ 563 void reconfigureIfAddr6(sdbusplus::bus::bus& bus, const ChannelParams& params, 564 uint8_t idx, const in6_addr& address, uint8_t prefix); 565 566 /** @brief Retrieves the current gateway for the address family on the system 567 * NOTE: The gateway is per channel instead of the system wide one. 568 * 569 * @param[in] bus - The bus object used for lookups 570 * @param[in] params - The parameters for the channel 571 * @return An address representing the gateway address if it exists 572 */ 573 template <int family> 574 std::optional<typename AddrFamily<family>::addr> 575 getGatewayProperty(sdbusplus::bus::bus& bus, const ChannelParams& params) 576 { 577 auto objPath = "/xyz/openbmc_project/network/" + params.ifname; 578 auto gatewayStr = std::get<std::string>( 579 getDbusProperty(bus, params.service, objPath, INTF_ETHERNET, 580 AddrFamily<family>::propertyGateway)); 581 if (gatewayStr.empty()) 582 { 583 return std::nullopt; 584 } 585 return stringToAddr<family>(gatewayStr.c_str()); 586 } 587 588 template <int family> 589 std::optional<IfNeigh<family>> 590 findStaticNeighbor(sdbusplus::bus::bus& bus, const ChannelParams& params, 591 const typename AddrFamily<family>::addr& ip, 592 ObjectLookupCache& neighbors) 593 { 594 using sdbusplus::xyz::openbmc_project::Network::server::Neighbor; 595 const auto state = 596 sdbusplus::xyz::openbmc_project::Network::server::convertForMessage( 597 Neighbor::State::Permanent); 598 for (const auto& [path, neighbor] : neighbors) 599 { 600 const auto& ipStr = std::get<std::string>(neighbor.at("IPAddress")); 601 auto neighIP = maybeStringToAddr<family>(ipStr.c_str()); 602 if (!neighIP) 603 { 604 continue; 605 } 606 if (!equal(*neighIP, ip)) 607 { 608 continue; 609 } 610 if (state != std::get<std::string>(neighbor.at("State"))) 611 { 612 continue; 613 } 614 615 IfNeigh<family> ret; 616 ret.path = path; 617 ret.ip = ip; 618 const auto& macStr = std::get<std::string>(neighbor.at("MACAddress")); 619 ret.mac = stringToMAC(macStr.c_str()); 620 return std::move(ret); 621 } 622 623 return std::nullopt; 624 } 625 626 template <int family> 627 void createNeighbor(sdbusplus::bus::bus& bus, const ChannelParams& params, 628 const typename AddrFamily<family>::addr& address, 629 const ether_addr& mac) 630 { 631 auto newreq = 632 bus.new_method_call(params.service.c_str(), params.logicalPath.c_str(), 633 INTF_NEIGHBOR_CREATE_STATIC, "Neighbor"); 634 std::string macStr = ether_ntoa(&mac); 635 newreq.append(addrToString<family>(address), macStr); 636 bus.call_noreply(newreq); 637 } 638 639 /** @brief Deletes the dbus object. Ignores empty objects or objects that are 640 * missing from the bus. 641 * 642 * @param[in] bus - The bus object used for lookups 643 * @param[in] service - The name of the service 644 * @param[in] path - The path of the object to delete 645 */ 646 void deleteObjectIfExists(sdbusplus::bus::bus& bus, const std::string& service, 647 const std::string& path); 648 649 /** @brief Sets the value for the default gateway of the channel 650 * 651 * @param[in] bus - The bus object used for lookups 652 * @param[in] params - The parameters for the channel 653 * @param[in] gateway - Gateway address to apply 654 */ 655 template <int family> 656 void setGatewayProperty(sdbusplus::bus::bus& bus, const ChannelParams& params, 657 const typename AddrFamily<family>::addr& address) 658 { 659 // Save the old gateway MAC address if it exists so we can recreate it 660 auto gateway = getGatewayProperty<family>(bus, params); 661 std::optional<IfNeigh<family>> neighbor; 662 if (gateway) 663 { 664 ObjectLookupCache neighbors(bus, params, INTF_NEIGHBOR); 665 neighbor = findStaticNeighbor<family>(bus, params, *gateway, neighbors); 666 } 667 668 auto objPath = "/xyz/openbmc_project/network/" + params.ifname; 669 setDbusProperty(bus, params.service, objPath, INTF_ETHERNET, 670 AddrFamily<family>::propertyGateway, 671 addrToString<family>(address)); 672 673 // Restore the gateway MAC if we had one 674 if (neighbor) 675 { 676 deleteObjectIfExists(bus, params.service, neighbor->path); 677 createNeighbor<family>(bus, params, address, neighbor->mac); 678 } 679 } 680 681 } // namespace transport 682 } // namespace ipmi 683