Tomáš Pecka | f10b930 | 2021-02-23 19:02:02 +0100 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2021 CESNET, https://photonics.cesnet.cz/ |
| 3 | * |
| 4 | * Written by Tomáš Pecka <tomas.pecka@cesnet.cz> |
| 5 | * |
| 6 | */ |
| 7 | |
Tomáš Pecka | 0972938 | 2021-03-08 19:36:50 +0100 | [diff] [blame] | 8 | #include <arpa/inet.h> |
Tomáš Pecka | f10b930 | 2021-02-23 19:02:02 +0100 | [diff] [blame] | 9 | #include <linux/if_arp.h> |
| 10 | #include <linux/netdevice.h> |
| 11 | #include "IETFInterfaces.h" |
| 12 | #include "Rtnetlink.h" |
| 13 | #include "utils/log.h" |
| 14 | #include "utils/sysrepo.h" |
| 15 | |
| 16 | using namespace std::string_literals; |
| 17 | |
| 18 | namespace { |
| 19 | |
Tomáš Pecka | b29f36b | 2021-03-31 20:02:06 +0200 | [diff] [blame^] | 20 | /** @brief Computes the length of the const C-string (array of const char) *including* the terminating zero |
| 21 | * |
| 22 | * Credits: https://dbj.org/cpp-zero-time-strlen-and-strnlen/ |
| 23 | */ |
| 24 | template <size_t N> |
| 25 | inline constexpr size_t arrlen(const char (&)[N]) noexcept |
| 26 | { |
| 27 | return N; |
| 28 | } |
| 29 | |
Tomáš Pecka | f10b930 | 2021-02-23 19:02:02 +0100 | [diff] [blame] | 30 | const auto CZECHLIGHT_NETWORK_MODULE_NAME = "czechlight-network"s; |
| 31 | const auto IETF_IP_MODULE_NAME = "ietf-ip"s; |
| 32 | const auto IETF_INTERFACES_MODULE_NAME = "ietf-interfaces"s; |
| 33 | const auto IETF_INTERFACES = "/"s + IETF_INTERFACES_MODULE_NAME + ":interfaces"s; |
| 34 | |
| 35 | const auto PHYS_ADDR_BUF_SIZE = 6 * 2 /* 2 chars per 6 bytes in the address */ + 5 /* delimiters (':') between bytes */ + 1 /* \0 */; |
Tomáš Pecka | b29f36b | 2021-03-31 20:02:06 +0200 | [diff] [blame^] | 36 | const auto IPV6ADDRSTRLEN_WITH_PREFIX = INET6_ADDRSTRLEN + 1 + 3 /* plus slash and max three-digits prefix */; |
Tomáš Pecka | f10b930 | 2021-02-23 19:02:02 +0100 | [diff] [blame] | 37 | |
| 38 | std::string operStatusToString(uint8_t operStatus, velia::Log log) |
| 39 | { |
| 40 | // unfortunately we can't use libnl's rtnl_link_operstate2str, because it creates different strings than the YANG model expects |
| 41 | switch (operStatus) { |
| 42 | case IF_OPER_UP: |
| 43 | return "up"; |
| 44 | case IF_OPER_DOWN: |
| 45 | return "down"; |
| 46 | case IF_OPER_TESTING: |
| 47 | return "testing"; |
| 48 | case IF_OPER_DORMANT: |
| 49 | return "dormant"; |
| 50 | case IF_OPER_NOTPRESENT: |
| 51 | return "not-present"; |
| 52 | case IF_OPER_LOWERLAYERDOWN: |
| 53 | return "lower-layer-down"; |
| 54 | case IF_OPER_UNKNOWN: |
| 55 | return "unknown"; |
| 56 | default: |
| 57 | log->warn("Encountered unknown operational status {}, using 'unknown'", operStatus); |
| 58 | return "unknown"; |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | std::string arpTypeToString(unsigned int arptype, velia::Log log) |
| 63 | { |
| 64 | switch (arptype) { |
| 65 | case ARPHRD_ETHER: |
| 66 | return "iana-if-type:ethernetCsmacd"; |
| 67 | case ARPHRD_LOOPBACK: |
| 68 | return "iana-if-type:softwareLoopback"; |
| 69 | case ARPHRD_SIT: |
| 70 | return "iana-if-type:sixToFour"; |
| 71 | default: |
| 72 | log->warn("Encountered unknown interface type {}, using 'iana-if-type:other'", arptype); |
| 73 | return "iana-if-type:other"; |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | std::string nlActionToString(int action) |
| 78 | { |
| 79 | switch (action) { |
| 80 | case NL_ACT_NEW: |
| 81 | return "NEW"; |
| 82 | case NL_ACT_DEL: |
| 83 | return "DEL"; |
| 84 | case NL_ACT_CHANGE: |
| 85 | return "CHANGE"; |
| 86 | case NL_ACT_UNSPEC: |
| 87 | return "UNSPEC"; |
| 88 | case NL_ACT_GET: |
| 89 | return "GET"; |
| 90 | case NL_ACT_SET: |
| 91 | return "SET"; |
| 92 | default: |
| 93 | return "<unknown action>"; |
| 94 | } |
| 95 | } |
| 96 | |
Tomáš Pecka | 0972938 | 2021-03-08 19:36:50 +0100 | [diff] [blame] | 97 | std::string binaddrToString(void* binaddr, int addrFamily) |
| 98 | { |
| 99 | // any IPv4 address fits into a buffer allocated for an IPv6 address |
| 100 | static_assert(INET6_ADDRSTRLEN >= INET_ADDRSTRLEN); |
| 101 | std::array<char, INET6_ADDRSTRLEN> buf; |
| 102 | |
| 103 | if (const char* res = inet_ntop(addrFamily, binaddr, buf.data(), buf.size()); res != nullptr) { |
| 104 | return res; |
| 105 | } else { |
| 106 | throw std::system_error {errno, std::generic_category(), "inet_ntop"}; |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | std::string getIPVersion(int addrFamily) |
| 111 | { |
| 112 | switch (addrFamily) { |
| 113 | case AF_INET: |
| 114 | return "ipv4"; |
| 115 | case AF_INET6: |
| 116 | return "ipv6"; |
| 117 | default: |
| 118 | throw std::runtime_error("Unexpected address family " + std::to_string(addrFamily)); |
| 119 | } |
| 120 | } |
| 121 | |
Tomáš Pecka | f10b930 | 2021-02-23 19:02:02 +0100 | [diff] [blame] | 122 | } |
| 123 | |
| 124 | namespace velia::system { |
| 125 | |
| 126 | IETFInterfaces::IETFInterfaces(std::shared_ptr<::sysrepo::Session> srSess) |
| 127 | : m_srSession(std::move(srSess)) |
| 128 | , m_log(spdlog::get("system")) |
Tomáš Pecka | 0972938 | 2021-03-08 19:36:50 +0100 | [diff] [blame] | 129 | , m_rtnetlink(std::make_shared<Rtnetlink>( |
| 130 | [this](rtnl_link* link, int action) { onLinkUpdate(link, action); }, |
Tomáš Pecka | b29f36b | 2021-03-31 20:02:06 +0200 | [diff] [blame^] | 131 | [this](rtnl_addr* addr, int action) { onAddrUpdate(addr, action); }, |
| 132 | [this](rtnl_route* addr, int action) { onRouteUpdate(addr, action); })) |
Tomáš Pecka | f10b930 | 2021-02-23 19:02:02 +0100 | [diff] [blame] | 133 | { |
| 134 | utils::ensureModuleImplemented(m_srSession, IETF_INTERFACES_MODULE_NAME, "2018-02-20"); |
| 135 | utils::ensureModuleImplemented(m_srSession, IETF_IP_MODULE_NAME, "2018-02-22"); |
| 136 | utils::ensureModuleImplemented(m_srSession, CZECHLIGHT_NETWORK_MODULE_NAME, "2021-02-22"); |
Tomáš Pecka | 3d3cf61 | 2021-03-31 19:51:31 +0200 | [diff] [blame] | 137 | |
| 138 | m_rtnetlink->invokeInitialCallbacks(); |
Tomáš Pecka | b29f36b | 2021-03-31 20:02:06 +0200 | [diff] [blame^] | 139 | // TODO: Implement /ietf-routing:routing/interfaces and /ietf-routing:routing/router-id |
Tomáš Pecka | f10b930 | 2021-02-23 19:02:02 +0100 | [diff] [blame] | 140 | } |
| 141 | |
| 142 | void IETFInterfaces::onLinkUpdate(rtnl_link* link, int action) |
| 143 | { |
| 144 | char* name = rtnl_link_get_name(link); |
| 145 | m_log->trace("Netlink update on link '{}', action {}", name, nlActionToString(action)); |
| 146 | |
| 147 | if (action == NL_ACT_DEL) { |
Tomáš Pecka | c9a57c3 | 2021-04-06 20:37:36 +0200 | [diff] [blame] | 148 | utils::valuesPush({}, {IETF_INTERFACES + "/interface[name='" + name + "']"}, m_srSession, SR_DS_OPERATIONAL); |
Tomáš Pecka | f10b930 | 2021-02-23 19:02:02 +0100 | [diff] [blame] | 149 | } else if (action == NL_ACT_CHANGE || action == NL_ACT_NEW) { |
| 150 | std::map<std::string, std::string> values; |
| 151 | std::vector<std::string> deletePaths; |
| 152 | |
Tomáš Pecka | db08413 | 2021-03-10 08:37:10 +0100 | [diff] [blame] | 153 | auto linkAddr = rtnl_link_get_addr(link); |
Tomáš Pecka | f10b930 | 2021-02-23 19:02:02 +0100 | [diff] [blame] | 154 | std::array<char, PHYS_ADDR_BUF_SIZE> buf; |
Tomáš Pecka | db08413 | 2021-03-10 08:37:10 +0100 | [diff] [blame] | 155 | if (auto physAddr = nl_addr2str(linkAddr, buf.data(), buf.size()); physAddr != "none"s && nl_addr_get_family(linkAddr) == AF_LLC) { // set physical address if the link has one |
Tomáš Pecka | f10b930 | 2021-02-23 19:02:02 +0100 | [diff] [blame] | 156 | values[IETF_INTERFACES + "/interface[name='" + name + "']/phys-address"] = physAddr; |
| 157 | } else { |
| 158 | // delete physical address from sysrepo if not provided by rtnetlink |
| 159 | // Note: During testing I have noticed that my wireless interface loses a physical address. There were several change callbacks invoked |
| 160 | // when simply bringing the interface down and up. In some of those, nl_addr2str returned "none". |
| 161 | deletePaths.push_back({IETF_INTERFACES + "/interface[name='" + name + "']/phys-address"}); |
| 162 | } |
| 163 | |
| 164 | values[IETF_INTERFACES + "/interface[name='" + name + "']/type"] = arpTypeToString(rtnl_link_get_arptype(link), m_log); |
| 165 | values[IETF_INTERFACES + "/interface[name='" + name + "']/oper-status"] = operStatusToString(rtnl_link_get_operstate(link), m_log); |
| 166 | |
| 167 | utils::valuesPush(values, deletePaths, m_srSession, SR_DS_OPERATIONAL); |
| 168 | } else { |
| 169 | m_log->warn("Unhandled cache update action {} ({})", action, nlActionToString(action)); |
| 170 | } |
| 171 | } |
| 172 | |
Tomáš Pecka | 0972938 | 2021-03-08 19:36:50 +0100 | [diff] [blame] | 173 | void IETFInterfaces::onAddrUpdate(rtnl_addr* addr, int action) |
| 174 | { |
| 175 | std::unique_ptr<rtnl_link, std::function<void(rtnl_link*)>> link(rtnl_addr_get_link(addr), [](rtnl_link* obj) { nl_object_put(OBJ_CAST(obj)); }); |
| 176 | |
| 177 | auto linkName = rtnl_link_get_name(link.get()); |
| 178 | auto addrFamily = rtnl_addr_get_family(addr); |
| 179 | if (addrFamily != AF_INET && addrFamily != AF_INET6) { |
| 180 | return; |
| 181 | } |
| 182 | |
| 183 | m_log->trace("Netlink update on address of link '{}', action {}", linkName, nlActionToString(action)); |
| 184 | |
| 185 | auto nlAddr = rtnl_addr_get_local(addr); |
| 186 | std::string ipAddress = binaddrToString(nl_addr_get_binary_addr(nlAddr), addrFamily); // We don't use libnl's nl_addr2str because it appends a prefix length to the string (e.g. 192.168.0.1/24) |
| 187 | std::string ipVersion = getIPVersion(addrFamily); |
| 188 | |
| 189 | std::map<std::string, std::string> values; |
| 190 | std::vector<std::string> deletePaths; |
| 191 | const auto yangPrefix = IETF_INTERFACES + "/interface[name='" + linkName + "']/ietf-ip:" + ipVersion + "/address[ip='" + ipAddress + "']"; |
| 192 | |
| 193 | if (action == NL_ACT_DEL) { |
| 194 | deletePaths.push_back({yangPrefix}); |
| 195 | } else if (action == NL_ACT_CHANGE || action == NL_ACT_NEW) { |
| 196 | values[yangPrefix + "/prefix-length"] = std::to_string(rtnl_addr_get_prefixlen(addr)); |
| 197 | } else { |
| 198 | m_log->warn("Unhandled cache update action {} ({})", action, nlActionToString(action)); |
| 199 | } |
| 200 | |
| 201 | utils::valuesPush(values, deletePaths, m_srSession, SR_DS_OPERATIONAL); |
| 202 | } |
Tomáš Pecka | b29f36b | 2021-03-31 20:02:06 +0200 | [diff] [blame^] | 203 | |
| 204 | void IETFInterfaces::onRouteUpdate(rtnl_route*, int) |
| 205 | { |
| 206 | /* NOTE: |
| 207 | * We don't know the position of the changed route in the list of routes |
| 208 | * Replace the whole subtree (and therefore fetch all routes to publish fresh data) |
| 209 | * Unfortunately, this function may be called several times during the "reconstruction" of the routing table. |
| 210 | */ |
| 211 | |
| 212 | std::map<std::string, std::string> values; |
| 213 | std::vector<std::string> deletePaths; |
| 214 | |
| 215 | auto routes = m_rtnetlink->getRoutes(); |
| 216 | auto links = m_rtnetlink->getLinks(); |
| 217 | |
| 218 | // ipv4 and ipv6 routes are in separate lists; keep a track of current index to the list so we correctly append the route to the end of the list |
| 219 | std::map<decltype(AF_INET), unsigned> routeIdx {{AF_INET, 1}, {AF_INET6, 1}}; |
| 220 | |
| 221 | for (const auto& route : routes) { |
| 222 | if (rtnl_route_get_table(route.get()) != RT_TABLE_MAIN) { |
| 223 | continue; |
| 224 | } |
| 225 | |
| 226 | if (rtnl_route_get_type(route.get()) != RTN_UNICAST) { |
| 227 | continue; |
| 228 | } |
| 229 | |
| 230 | auto family = rtnl_route_get_family(route.get()); |
| 231 | if (family != AF_INET && family != AF_INET6) { |
| 232 | continue; |
| 233 | } |
| 234 | |
| 235 | auto proto = rtnl_route_get_protocol(route.get()); |
| 236 | if (proto != RTPROT_KERNEL && proto != RTPROT_RA && proto != RTPROT_DHCP && proto != RTPROT_STATIC && proto != RTPROT_BOOT) { |
| 237 | std::array<char, arrlen("redirect")> buf; /* "redirect" is the longest value (libnl/lib/route/route_utils.c, init_proto_names) */ |
| 238 | m_log->warn("Unimplemented routing protocol {} '{}'", proto, rtnl_route_proto2str(proto, buf.data(), buf.size())); |
| 239 | continue; |
| 240 | } |
| 241 | |
| 242 | const auto ribName = family == AF_INET ? "ipv4-master"s : "ipv6-master"s; |
| 243 | const auto yangPrefix = "/ietf-routing:routing/ribs/rib[name='" + ribName + "']/routes/route["s + std::to_string(routeIdx[family]++) + "]/"; |
| 244 | const auto familyYangPrefix = family == AF_INET ? "ietf-ipv4-unicast-routing"s : "ietf-ipv6-unicast-routing"s; |
| 245 | |
| 246 | std::string destPrefix; |
| 247 | if (auto* addr = rtnl_route_get_dst(route.get()); addr != nullptr) { |
| 248 | if (nl_addr_iszero(addr)) { |
| 249 | destPrefix = family == AF_INET ? "0.0.0.0/0" : "::/0"; |
| 250 | } else { |
| 251 | std::array<char, IPV6ADDRSTRLEN_WITH_PREFIX> data; |
| 252 | destPrefix = nl_addr2str(addr, data.data(), data.size()); |
| 253 | |
| 254 | // append prefix len if nl_addr2str fails to do that (when prefix length is 32 in ipv4 or 128 in ipv6) |
| 255 | if (destPrefix.find_first_of('/') == std::string::npos) { |
| 256 | destPrefix += "/" + std::to_string(nl_addr_get_prefixlen(addr)); |
| 257 | } |
| 258 | } |
| 259 | } |
| 260 | |
| 261 | values[yangPrefix + familyYangPrefix + ":destination-prefix"] = destPrefix; |
| 262 | |
| 263 | auto scope = rtnl_route_get_scope(route.get()); |
| 264 | std::string protoStr; |
| 265 | switch (proto) { |
| 266 | case RTPROT_KERNEL: |
| 267 | protoStr = scope == RT_SCOPE_LINK ? "direct" : "static"; |
| 268 | break; |
| 269 | case RTPROT_STATIC: |
| 270 | case RTPROT_BOOT: |
| 271 | protoStr = "static"; |
| 272 | break; |
| 273 | case RTPROT_DHCP: |
| 274 | protoStr = "czechlight-network:dhcp"; |
| 275 | break; |
| 276 | case RTPROT_RA: |
| 277 | protoStr = "czechlight-network:ra"; |
| 278 | break; |
| 279 | default: |
| 280 | throw std::invalid_argument("Unexpected route protocol ("s + std::to_string(proto) + ")"); |
| 281 | } |
| 282 | |
| 283 | values[yangPrefix + "source-protocol"] = protoStr; |
| 284 | |
| 285 | const auto hops = rtnl_route_get_nnexthops(route.get()); |
| 286 | const bool multihop = hops > 1; |
| 287 | for (auto i = 0; i < hops; i++) { |
| 288 | rtnl_nexthop* nh = rtnl_route_nexthop_n(route.get(), i); |
| 289 | |
| 290 | if (nl_addr* addr = rtnl_route_nh_get_gateway(nh); addr) { |
| 291 | std::string yangKey; |
| 292 | if (!multihop) { |
| 293 | yangKey = yangPrefix + "next-hop/" + familyYangPrefix + ":next-hop-address"; |
| 294 | } else { |
| 295 | yangKey = yangPrefix + "next-hop/next-hop-list/next-hop[" + std::to_string(i + 1) + "]/" + familyYangPrefix + ":address"; |
| 296 | } |
| 297 | |
| 298 | std::array<char, IPV6ADDRSTRLEN_WITH_PREFIX> buf; |
| 299 | values[yangKey] = nl_addr2str(addr, buf.data(), buf.size()); |
| 300 | } |
| 301 | |
| 302 | auto if_index = rtnl_route_nh_get_ifindex(nh); |
| 303 | if (auto linkIt = std::find_if(links.begin(), links.end(), [if_index](const Rtnetlink::nlLink& link) { return rtnl_link_get_ifindex(link.get()) == if_index; }); linkIt != links.end()) { |
| 304 | if (char* ifname = rtnl_link_get_name(linkIt->get()); ifname) { |
| 305 | std::string yangKey; |
| 306 | if (!multihop) { |
| 307 | yangKey = yangPrefix + "next-hop/outgoing-interface"; |
| 308 | } else { |
| 309 | yangKey = yangPrefix + "next-hop/next-hop-list/next-hop[" + std::to_string(i + 1) + "]/outgoing-interface"; |
| 310 | } |
| 311 | |
| 312 | values[yangKey] = rtnl_link_get_name(linkIt->get()); |
| 313 | } |
| 314 | } |
| 315 | } |
| 316 | } |
| 317 | |
| 318 | utils::valuesPush(values, deletePaths, m_srSession, SR_DS_OPERATIONAL); |
| 319 | } |
Tomáš Pecka | f10b930 | 2021-02-23 19:02:02 +0100 | [diff] [blame] | 320 | } |