Tomáš Pecka | 7acf392 | 2021-08-10 11:16:57 +0200 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2020 CESNET, https://photonics.cesnet.cz/ |
| 3 | * |
| 4 | * Written by Tomáš Pecka <tomas.pecka@fit.cvut.cz> |
| 5 | * |
| 6 | */ |
| 7 | #include <netinet/ether.h> |
| 8 | #include <spdlog/spdlog.h> |
| 9 | #include "LLDP.h" |
| 10 | #include "utils/log.h" |
| 11 | |
| 12 | namespace velia::system { |
| 13 | |
Tomáš Pecka | 36427a1 | 2021-08-10 11:27:56 +0200 | [diff] [blame] | 14 | namespace { |
Tomáš Pecka | 7acf392 | 2021-08-10 11:16:57 +0200 | [diff] [blame] | 15 | |
| 16 | static const std::string systemdNetworkdDbusInterface = "org.freedesktop.network1.Manager"; |
| 17 | static const sdbus::ObjectPath systemdNetworkdDbusManagerObjectPath = "/org/freedesktop/network1"; |
| 18 | |
| 19 | /** @brief LLDP capabilities identifiers ordered by their appearence in YANG schema 'czechlight-lldp' */ |
| 20 | const char* SYSTEM_CAPABILITIES[] = { |
| 21 | "other", |
| 22 | "repeater", |
| 23 | "bridge", |
| 24 | "wlan-access-point", |
| 25 | "router", |
| 26 | "telephone", |
| 27 | "docsis-cable-device", |
| 28 | "station-only", |
| 29 | "cvlan-component", |
| 30 | "svlan-component", |
| 31 | "two-port-mac-relay", |
| 32 | }; |
| 33 | |
| 34 | /** @brief Converts systemd's capabilities bitset to YANG's (named) bits. |
| 35 | * |
| 36 | * Apparently, libyang's parser requires the bits to be specified as string of names separated by whitespace. |
| 37 | * See libyang's src/parser.c (function lyp_parse_value, switch-case LY_TYPE_BITS) and tests/test_sec9_7.c |
| 38 | * |
| 39 | * The names of individual bits should appear in the order they are defined in the YANG schema. At least that is how |
| 40 | * I understand libyang's comment 'identifiers appear ordered by their position' in src/parser.c. |
| 41 | * Systemd and our YANG model czechlight-lldp define the bits in the same order so this function does not have to care |
| 42 | * about it. |
| 43 | */ |
| 44 | std::string toBitsYANG(uint16_t bits) |
| 45 | { |
| 46 | std::string res; |
| 47 | |
| 48 | unsigned idx = 0; |
| 49 | while (bits) { |
| 50 | if (bits % 2) { |
| 51 | if (!res.empty()) { |
| 52 | res += " "; |
| 53 | } |
| 54 | res += SYSTEM_CAPABILITIES[idx]; |
| 55 | } |
| 56 | |
| 57 | bits /= 2; |
| 58 | idx += 1; |
| 59 | } |
| 60 | |
| 61 | return res; |
| 62 | } |
| 63 | |
| 64 | /** @brief sd_lldp_neighbor requires deletion by invoking sd_lldp_neighbor_unrefp */ |
| 65 | struct sd_lldp_neighbor_deleter { |
| 66 | void operator()(sd_lldp_neighbor* e) const |
| 67 | { |
| 68 | sd_lldp_neighbor_unrefp(&e); |
| 69 | } |
| 70 | }; |
| 71 | using sd_lldp_neighbor_managed = std::unique_ptr<sd_lldp_neighbor, sd_lldp_neighbor_deleter>; |
| 72 | |
| 73 | /* @brief Reads a LLDP neighbour entry from systemd's binary LLDP files. |
| 74 | * |
| 75 | * Inspired systemd's networkctl code. |
| 76 | */ |
| 77 | sd_lldp_neighbor_managed nextNeighbor(std::ifstream& ifs) |
| 78 | { |
| 79 | size_t size; |
| 80 | |
| 81 | // read neighbor size |
| 82 | /* Systemd allows the LLDP frame to be at most 4 KiB long. The comment in networkctl.c states that |
| 83 | * "each LLDP packet is at most MTU size, but let's allow up to 4KiB just in case". |
| 84 | * This comment may be misleading a bit because Ethernet Jumbo Frames can be up to 9000 B long. |
| 85 | * However, LLDP frames should still be at most 1500 B long. |
| 86 | * (see https://www.cisco.com/c/en/us/td/docs/routers/ncs4000/software/configure/guide/configurationguide/configurationguide_chapter_0111011.pdf) |
| 87 | */ |
| 88 | { |
| 89 | uint64_t rawSz; // get neighbour size in bytes |
| 90 | ifs.read(reinterpret_cast<char*>(&rawSz), sizeof(rawSz)); |
| 91 | size = le64toh(rawSz); |
| 92 | |
| 93 | if (size_t rd = ifs.gcount(); (rd == 0 && ifs.eof()) || rd != sizeof(rawSz) || size >= 4096) { |
| 94 | return nullptr; |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | std::vector<uint8_t> raw; |
| 99 | raw.resize(size); |
| 100 | |
| 101 | ifs.read(reinterpret_cast<char*>(raw.data()), size); |
| 102 | if (static_cast<size_t>(ifs.gcount()) != size) { // typecast is safe here (see std::streamsize) |
| 103 | return nullptr; |
| 104 | } |
| 105 | |
| 106 | // let systemd parse from raw |
| 107 | sd_lldp_neighbor* tmp = nullptr; |
| 108 | if (sd_lldp_neighbor_from_raw(&tmp, raw.data(), size) < 0) { |
| 109 | return nullptr; |
| 110 | } |
| 111 | |
| 112 | return sd_lldp_neighbor_managed(tmp); |
| 113 | } |
| 114 | |
| 115 | /* @brief Lists links using networkd dbus interface and returns them as a list of pairs <link_id, link_name>. */ |
| 116 | auto listLinks(sdbus::IProxy* networkdManagerProxy) |
| 117 | { |
| 118 | std::vector<sdbus::Struct<int, std::string, sdbus::ObjectPath>> links; |
| 119 | std::vector<std::pair<int, std::string>> res; // we only want to return pairs (linkId, linkName), we do not need dbus object path |
| 120 | |
Tomáš Pecka | 36427a1 | 2021-08-10 11:27:56 +0200 | [diff] [blame] | 121 | networkdManagerProxy->callMethod("ListLinks").onInterface(systemdNetworkdDbusInterface).storeResultsTo(links); |
Tomáš Pecka | 7acf392 | 2021-08-10 11:16:57 +0200 | [diff] [blame] | 122 | |
| 123 | std::transform(links.begin(), links.end(), std::back_inserter(res), [](const auto& e) { return std::make_pair(std::get<0>(e), std::get<1>(e)); }); |
| 124 | return res; |
| 125 | } |
| 126 | |
Tomáš Pecka | 36427a1 | 2021-08-10 11:27:56 +0200 | [diff] [blame] | 127 | } |
Tomáš Pecka | 7acf392 | 2021-08-10 11:16:57 +0200 | [diff] [blame] | 128 | |
| 129 | LLDPDataProvider::LLDPDataProvider(std::filesystem::path dataDirectory, sdbus::IConnection& dbusConnection, const std::string& dbusNetworkdBus) |
| 130 | : m_log(spdlog::get("system")) |
| 131 | , m_dataDirectory(std::move(dataDirectory)) |
Tomáš Pecka | 36427a1 | 2021-08-10 11:27:56 +0200 | [diff] [blame] | 132 | , m_networkdDbusProxy(sdbus::createProxy(dbusConnection, dbusNetworkdBus, systemdNetworkdDbusManagerObjectPath)) |
Tomáš Pecka | 7acf392 | 2021-08-10 11:16:57 +0200 | [diff] [blame] | 133 | { |
| 134 | } |
| 135 | |
| 136 | std::vector<NeighborEntry> LLDPDataProvider::getNeighbors() const |
| 137 | { |
| 138 | std::vector<NeighborEntry> res; |
| 139 | |
Tomáš Pecka | 36427a1 | 2021-08-10 11:27:56 +0200 | [diff] [blame] | 140 | for (const auto& [linkId, linkName] : listLinks(m_networkdDbusProxy.get())) { |
Tomáš Pecka | 7acf392 | 2021-08-10 11:16:57 +0200 | [diff] [blame] | 141 | m_log->debug("LLDP: Collecting neighbours on '{}' (id {})", linkName, linkId); |
| 142 | |
| 143 | // open lldp datafile |
| 144 | std::filesystem::path lldpFilename = m_dataDirectory / std::to_string(linkId); |
| 145 | std::ifstream ifs(lldpFilename, std::ios::binary); |
| 146 | |
| 147 | if (!ifs.is_open()) { |
| 148 | // TODO: As of now, we are querying systemd-networkd for *all* links, not just those that have LLDP enabled. |
| 149 | // TODO: Create a patch for systemd that queries *only* links with LLDP enabled and change severity of this debug log to warning/error. |
| 150 | m_log->debug(" failed to open ({})", lldpFilename); |
| 151 | continue; |
| 152 | } |
| 153 | |
Tomáš Pecka | 36427a1 | 2021-08-10 11:27:56 +0200 | [diff] [blame] | 154 | while (auto n = nextNeighbor(ifs)) { |
Tomáš Pecka | 7acf392 | 2021-08-10 11:16:57 +0200 | [diff] [blame] | 155 | NeighborEntry ne; |
| 156 | ne.m_portId = linkName; |
| 157 | |
| 158 | if (const char* system_name = nullptr; sd_lldp_neighbor_get_system_name(n.get(), &system_name) >= 0) { |
| 159 | ne.m_properties["remoteSysName"] = system_name; |
| 160 | } |
| 161 | if (const char* port_id = nullptr; sd_lldp_neighbor_get_port_id_as_string(n.get(), &port_id) >= 0) { |
| 162 | ne.m_properties["remotePortId"] = port_id; |
| 163 | } |
| 164 | if (const char* chassis_id = nullptr; sd_lldp_neighbor_get_chassis_id_as_string(n.get(), &chassis_id) >= 0) { |
| 165 | ne.m_properties["remoteChassisId"] = chassis_id; |
| 166 | } |
| 167 | if (ether_addr* addr = nullptr; sd_lldp_neighbor_get_destination_address(n.get(), addr) >= 0) { |
| 168 | ne.m_properties["remoteMgmtAddress"] = ether_ntoa(addr); |
| 169 | } |
| 170 | |
| 171 | if (uint16_t cap = 0; sd_lldp_neighbor_get_system_capabilities(n.get(), &cap) >= 0) { |
Tomáš Pecka | 36427a1 | 2021-08-10 11:27:56 +0200 | [diff] [blame] | 172 | ne.m_properties["systemCapabilitiesSupported"] = toBitsYANG(cap); |
Tomáš Pecka | 7acf392 | 2021-08-10 11:16:57 +0200 | [diff] [blame] | 173 | } |
| 174 | |
| 175 | if (uint16_t cap = 0; sd_lldp_neighbor_get_enabled_capabilities(n.get(), &cap) >= 0) { |
Tomáš Pecka | 36427a1 | 2021-08-10 11:27:56 +0200 | [diff] [blame] | 176 | ne.m_properties["systemCapabilitiesEnabled"] = toBitsYANG(cap); |
Tomáš Pecka | 7acf392 | 2021-08-10 11:16:57 +0200 | [diff] [blame] | 177 | } |
| 178 | |
| 179 | m_log->trace(" found neighbor {}", ne); |
| 180 | res.push_back(ne); |
| 181 | } |
| 182 | } |
| 183 | |
| 184 | return res; |
| 185 | } |
| 186 | |
| 187 | std::ostream& operator<<(std::ostream& os, const NeighborEntry& entry) |
| 188 | { |
| 189 | os << "NeighborEntry(" << entry.m_portId << ": {"; |
| 190 | |
| 191 | for (auto it = entry.m_properties.begin(); it != entry.m_properties.end(); ++it) { |
| 192 | if (it != entry.m_properties.begin()) { |
| 193 | os << ", "; |
| 194 | } |
| 195 | |
| 196 | os << it->first << ": " << it->second; |
| 197 | } |
| 198 | |
| 199 | return os << "}"; |
| 200 | } |
| 201 | |
| 202 | } |