blob: 6262e9bbc570d8b15ba3ffbe9ae0f4886e8ed643 [file] [log] [blame]
Tomáš Peckaf10b9302021-02-23 19:02:02 +01001/*
2 * Copyright (C) 2021 CESNET, https://photonics.cesnet.cz/
3 *
4 * Written by Tomáš Pecka <tomas.pecka@cesnet.cz>
5 *
6 */
7
Tomáš Pecka09729382021-03-08 19:36:50 +01008#include <arpa/inet.h>
Tomáš Peckaf9108932021-06-01 10:19:36 +02009#include <filesystem>
Tomáš Peckaf10b9302021-02-23 19:02:02 +010010#include <linux/if_arp.h>
11#include <linux/netdevice.h>
12#include "IETFInterfaces.h"
13#include "Rtnetlink.h"
14#include "utils/log.h"
15#include "utils/sysrepo.h"
16
17using namespace std::string_literals;
18
19namespace {
20
Tomáš Peckab29f36b2021-03-31 20:02:06 +020021/** @brief Computes the length of the const C-string (array of const char) *including* the terminating zero
22 *
23 * Credits: https://dbj.org/cpp-zero-time-strlen-and-strnlen/
24 */
25template <size_t N>
26inline constexpr size_t arrlen(const char (&)[N]) noexcept
27{
28 return N;
29}
30
Tomáš Peckaf10b9302021-02-23 19:02:02 +010031const auto CZECHLIGHT_NETWORK_MODULE_NAME = "czechlight-network"s;
32const auto IETF_IP_MODULE_NAME = "ietf-ip"s;
33const auto IETF_INTERFACES_MODULE_NAME = "ietf-interfaces"s;
Tomáš Pecka3611c0e2021-04-14 09:02:14 +020034const auto IETF_ROUTING_MODULE_NAME = "ietf-routing"s;
35const auto IETF_IPV4_UNICAST_ROUTING_MODULE_NAME = "ietf-ipv4-unicast-routing";
36const auto IETF_IPV6_UNICAST_ROUTING_MODULE_NAME = "ietf-ipv6-unicast-routing";
Tomáš Peckaf10b9302021-02-23 19:02:02 +010037const auto IETF_INTERFACES = "/"s + IETF_INTERFACES_MODULE_NAME + ":interfaces"s;
38
39const auto PHYS_ADDR_BUF_SIZE = 6 * 2 /* 2 chars per 6 bytes in the address */ + 5 /* delimiters (':') between bytes */ + 1 /* \0 */;
Tomáš Peckab29f36b2021-03-31 20:02:06 +020040const auto IPV6ADDRSTRLEN_WITH_PREFIX = INET6_ADDRSTRLEN + 1 + 3 /* plus slash and max three-digits prefix */;
Tomáš Peckaf10b9302021-02-23 19:02:02 +010041
42std::string operStatusToString(uint8_t operStatus, velia::Log log)
43{
44 // unfortunately we can't use libnl's rtnl_link_operstate2str, because it creates different strings than the YANG model expects
45 switch (operStatus) {
46 case IF_OPER_UP:
47 return "up";
48 case IF_OPER_DOWN:
49 return "down";
50 case IF_OPER_TESTING:
51 return "testing";
52 case IF_OPER_DORMANT:
53 return "dormant";
54 case IF_OPER_NOTPRESENT:
55 return "not-present";
56 case IF_OPER_LOWERLAYERDOWN:
57 return "lower-layer-down";
58 case IF_OPER_UNKNOWN:
59 return "unknown";
60 default:
61 log->warn("Encountered unknown operational status {}, using 'unknown'", operStatus);
62 return "unknown";
63 }
64}
65
66std::string arpTypeToString(unsigned int arptype, velia::Log log)
67{
68 switch (arptype) {
69 case ARPHRD_ETHER:
70 return "iana-if-type:ethernetCsmacd";
71 case ARPHRD_LOOPBACK:
72 return "iana-if-type:softwareLoopback";
73 case ARPHRD_SIT:
74 return "iana-if-type:sixToFour";
75 default:
76 log->warn("Encountered unknown interface type {}, using 'iana-if-type:other'", arptype);
77 return "iana-if-type:other";
78 }
79}
80
81std::string nlActionToString(int action)
82{
83 switch (action) {
84 case NL_ACT_NEW:
85 return "NEW";
86 case NL_ACT_DEL:
87 return "DEL";
88 case NL_ACT_CHANGE:
89 return "CHANGE";
90 case NL_ACT_UNSPEC:
91 return "UNSPEC";
92 case NL_ACT_GET:
93 return "GET";
94 case NL_ACT_SET:
95 return "SET";
96 default:
97 return "<unknown action>";
98 }
99}
100
Tomáš Pecka09729382021-03-08 19:36:50 +0100101std::string binaddrToString(void* binaddr, int addrFamily)
102{
103 // any IPv4 address fits into a buffer allocated for an IPv6 address
104 static_assert(INET6_ADDRSTRLEN >= INET_ADDRSTRLEN);
Václav Kubernáte407f742021-05-18 10:47:13 +0200105 std::array<char, INET6_ADDRSTRLEN> buf{};
Tomáš Pecka09729382021-03-08 19:36:50 +0100106
107 if (const char* res = inet_ntop(addrFamily, binaddr, buf.data(), buf.size()); res != nullptr) {
108 return res;
109 } else {
110 throw std::system_error {errno, std::generic_category(), "inet_ntop"};
111 }
112}
113
114std::string getIPVersion(int addrFamily)
115{
116 switch (addrFamily) {
117 case AF_INET:
118 return "ipv4";
119 case AF_INET6:
120 return "ipv6";
121 default:
122 throw std::runtime_error("Unexpected address family " + std::to_string(addrFamily));
123 }
124}
125
Tomáš Pecka3663aae2021-03-10 13:46:31 +0100126/** @brief Returns YANG structure for ietf-ip:ipv(4|6)/neighbours. Set requestedAddrFamily to required ip version (AF_INET for ipv4 or AF_INET6 for ipv6). */
127std::map<std::string, std::string> collectNeighboursIP(std::shared_ptr<velia::system::Rtnetlink> rtnetlink, int requestedAddrFamily, velia::Log log)
128{
129 std::map<std::string, std::string> values;
130
131 for (const auto& [neigh, link] : rtnetlink->getNeighbours()) {
132 if (rtnl_neigh_get_state(neigh.get()) == NUD_NOARP) {
133 continue;
134 }
135
136 auto linkName = rtnl_link_get_name(link.get());
137
138 auto ipAddr = rtnl_neigh_get_dst(neigh.get());
139 auto ipAddrFamily = nl_addr_get_family(ipAddr);
140
141 if (ipAddrFamily != requestedAddrFamily) {
142 continue;
143 }
144
145 auto ipAddress = binaddrToString(nl_addr_get_binary_addr(ipAddr), ipAddrFamily);
146
147 auto llAddr = rtnl_neigh_get_lladdr(neigh.get());
148 std::array<char, PHYS_ADDR_BUF_SIZE> llAddrBuf {};
149 if (auto llAddress = nl_addr2str(llAddr, llAddrBuf.data(), llAddrBuf.size()); llAddress != "none"s) {
150 values[IETF_INTERFACES + "/interface[name='" + linkName + "']/ietf-ip:" + getIPVersion(ipAddrFamily) + "/neighbor[ip='" + ipAddress + "']/link-layer-address"] = llAddress;
151 } else {
152 log->warn("Neighbor '{}' on link '{}' returned link layer address 'none'", ipAddress, linkName);
153 }
154 }
155
156 return values;
157}
Tomáš Peckaf9108932021-06-01 10:19:36 +0200158
159/** @brief Determine if link is a bridge
160 *
161 * This is done via sysfs query because rtnl_link_is_bridge doesn't always work. When bridge ports are being added/removed, kernel issues a rtnetlink message
162 * RTM_NEWLINK/RTM_DELLINK which is not a complete message. It is just an information that a bridge port changed. The rtnl_link object created by libnl from
163 * that message is not fully instantiated and rtnl_link_is_bridge function considers it a bridge.
164 *
165 * See git log for details and references.
166 */
167bool isBridge(rtnl_link* link)
168{
169 return std::filesystem::exists("/sys/class/net/"s + rtnl_link_get_name(link) + "/bridge");
170}
Tomáš Peckaf10b9302021-02-23 19:02:02 +0100171}
172
173namespace velia::system {
174
175IETFInterfaces::IETFInterfaces(std::shared_ptr<::sysrepo::Session> srSess)
176 : m_srSession(std::move(srSess))
Tomáš Pecka70e54562021-03-10 12:39:03 +0100177 , m_srSubscribe(std::make_shared<::sysrepo::Subscribe>(m_srSession))
Tomáš Peckaf10b9302021-02-23 19:02:02 +0100178 , m_log(spdlog::get("system"))
Tomáš Pecka09729382021-03-08 19:36:50 +0100179 , m_rtnetlink(std::make_shared<Rtnetlink>(
180 [this](rtnl_link* link, int action) { onLinkUpdate(link, action); },
Tomáš Peckab29f36b2021-03-31 20:02:06 +0200181 [this](rtnl_addr* addr, int action) { onAddrUpdate(addr, action); },
182 [this](rtnl_route* addr, int action) { onRouteUpdate(addr, action); }))
Tomáš Peckaf10b9302021-02-23 19:02:02 +0100183{
184 utils::ensureModuleImplemented(m_srSession, IETF_INTERFACES_MODULE_NAME, "2018-02-20");
185 utils::ensureModuleImplemented(m_srSession, IETF_IP_MODULE_NAME, "2018-02-22");
Tomáš Pecka3611c0e2021-04-14 09:02:14 +0200186 utils::ensureModuleImplemented(m_srSession, IETF_ROUTING_MODULE_NAME, "2018-03-13");
187 utils::ensureModuleImplemented(m_srSession, IETF_IPV4_UNICAST_ROUTING_MODULE_NAME, "2018-03-13");
188 utils::ensureModuleImplemented(m_srSession, IETF_IPV6_UNICAST_ROUTING_MODULE_NAME, "2018-03-13");
Tomáš Peckaf10b9302021-02-23 19:02:02 +0100189 utils::ensureModuleImplemented(m_srSession, CZECHLIGHT_NETWORK_MODULE_NAME, "2021-02-22");
Tomáš Pecka3d3cf612021-03-31 19:51:31 +0200190
191 m_rtnetlink->invokeInitialCallbacks();
Tomáš Peckab29f36b2021-03-31 20:02:06 +0200192 // TODO: Implement /ietf-routing:routing/interfaces and /ietf-routing:routing/router-id
Tomáš Pecka70e54562021-03-10 12:39:03 +0100193
194 m_srSubscribe->oper_get_items_subscribe(
195 IETF_INTERFACES_MODULE_NAME.c_str(), [this](auto session, auto, auto, auto, auto, auto& parent) {
196 std::map<std::string, std::string> values;
197 for (const auto& link : m_rtnetlink->getLinks()) {
198 const auto yangPrefix = IETF_INTERFACES + "/interface[name='" + rtnl_link_get_name(link.get()) + "']/statistics";
199
200 values[yangPrefix + "/in-octets"] = std::to_string(rtnl_link_get_stat(link.get(), RTNL_LINK_RX_BYTES));
201 values[yangPrefix + "/out-octets"] = std::to_string(rtnl_link_get_stat(link.get(), RTNL_LINK_TX_BYTES));
202 values[yangPrefix + "/in-discards"] = std::to_string(rtnl_link_get_stat(link.get(), RTNL_LINK_RX_DROPPED));
203 values[yangPrefix + "/out-discards"] = std::to_string(rtnl_link_get_stat(link.get(), RTNL_LINK_TX_DROPPED));
204 values[yangPrefix + "/in-errors"] = std::to_string(rtnl_link_get_stat(link.get(), RTNL_LINK_RX_ERRORS));
205 values[yangPrefix + "/out-errors"] = std::to_string(rtnl_link_get_stat(link.get(), RTNL_LINK_TX_ERRORS));
206 }
207
208 utils::valuesToYang(values, {}, session, parent);
209 return SR_ERR_OK;
210 },
211 (IETF_INTERFACES + "/interface/statistics").c_str());
Tomáš Pecka3663aae2021-03-10 13:46:31 +0100212
213 m_srSubscribe->oper_get_items_subscribe(
214 IETF_INTERFACES_MODULE_NAME.c_str(), [this](auto session, auto, auto, auto, auto, auto& parent) {
215 utils::valuesToYang(collectNeighboursIP(m_rtnetlink, AF_INET, m_log), {}, session, parent);
216 return SR_ERR_OK;
217 },
218 (IETF_INTERFACES + "/interface/ietf-ip:ipv4/neighbor").c_str());
219
220 m_srSubscribe->oper_get_items_subscribe(
221 IETF_INTERFACES_MODULE_NAME.c_str(), [this](auto session, auto, auto, auto, auto, auto& parent) {
222 utils::valuesToYang(collectNeighboursIP(m_rtnetlink, AF_INET6, m_log), {}, session, parent);
223 return SR_ERR_OK;
224 },
225 (IETF_INTERFACES + "/interface/ietf-ip:ipv6/neighbor").c_str());
Tomáš Peckaf10b9302021-02-23 19:02:02 +0100226}
227
228void IETFInterfaces::onLinkUpdate(rtnl_link* link, int action)
229{
230 char* name = rtnl_link_get_name(link);
231 m_log->trace("Netlink update on link '{}', action {}", name, nlActionToString(action));
232
233 if (action == NL_ACT_DEL) {
Tomáš Pecka53f08ee2021-04-28 12:38:11 +0200234 utils::valuesPush(std::vector<utils::YANGPair>{}, {IETF_INTERFACES + "/interface[name='" + name + "']"}, m_srSession, SR_DS_OPERATIONAL);
Tomáš Peckaf10b9302021-02-23 19:02:02 +0100235 } else if (action == NL_ACT_CHANGE || action == NL_ACT_NEW) {
236 std::map<std::string, std::string> values;
237 std::vector<std::string> deletePaths;
238
Tomáš Peckadb084132021-03-10 08:37:10 +0100239 auto linkAddr = rtnl_link_get_addr(link);
Tomáš Peckaf10b9302021-02-23 19:02:02 +0100240 std::array<char, PHYS_ADDR_BUF_SIZE> buf;
Tomáš Peckadb084132021-03-10 08:37:10 +0100241 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áš Peckaf10b9302021-02-23 19:02:02 +0100242 values[IETF_INTERFACES + "/interface[name='" + name + "']/phys-address"] = physAddr;
243 } else {
244 // delete physical address from sysrepo if not provided by rtnetlink
245 // Note: During testing I have noticed that my wireless interface loses a physical address. There were several change callbacks invoked
246 // when simply bringing the interface down and up. In some of those, nl_addr2str returned "none".
247 deletePaths.push_back({IETF_INTERFACES + "/interface[name='" + name + "']/phys-address"});
248 }
249
Tomáš Peckaf9108932021-06-01 10:19:36 +0200250 values[IETF_INTERFACES + "/interface[name='" + name + "']/type"] = isBridge(link) ? "iana-if-type:bridge" : arpTypeToString(rtnl_link_get_arptype(link), m_log);
Tomáš Peckaf10b9302021-02-23 19:02:02 +0100251 values[IETF_INTERFACES + "/interface[name='" + name + "']/oper-status"] = operStatusToString(rtnl_link_get_operstate(link), m_log);
252
253 utils::valuesPush(values, deletePaths, m_srSession, SR_DS_OPERATIONAL);
254 } else {
255 m_log->warn("Unhandled cache update action {} ({})", action, nlActionToString(action));
256 }
257}
258
Tomáš Pecka09729382021-03-08 19:36:50 +0100259void IETFInterfaces::onAddrUpdate(rtnl_addr* addr, int action)
260{
261 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)); });
262
263 auto linkName = rtnl_link_get_name(link.get());
264 auto addrFamily = rtnl_addr_get_family(addr);
265 if (addrFamily != AF_INET && addrFamily != AF_INET6) {
266 return;
267 }
268
269 m_log->trace("Netlink update on address of link '{}', action {}", linkName, nlActionToString(action));
270
271 auto nlAddr = rtnl_addr_get_local(addr);
272 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)
273 std::string ipVersion = getIPVersion(addrFamily);
274
275 std::map<std::string, std::string> values;
276 std::vector<std::string> deletePaths;
277 const auto yangPrefix = IETF_INTERFACES + "/interface[name='" + linkName + "']/ietf-ip:" + ipVersion + "/address[ip='" + ipAddress + "']";
278
279 if (action == NL_ACT_DEL) {
280 deletePaths.push_back({yangPrefix});
281 } else if (action == NL_ACT_CHANGE || action == NL_ACT_NEW) {
282 values[yangPrefix + "/prefix-length"] = std::to_string(rtnl_addr_get_prefixlen(addr));
283 } else {
284 m_log->warn("Unhandled cache update action {} ({})", action, nlActionToString(action));
285 }
286
287 utils::valuesPush(values, deletePaths, m_srSession, SR_DS_OPERATIONAL);
288}
Tomáš Peckab29f36b2021-03-31 20:02:06 +0200289
290void IETFInterfaces::onRouteUpdate(rtnl_route*, int)
291{
292 /* NOTE:
293 * We don't know the position of the changed route in the list of routes
294 * Replace the whole subtree (and therefore fetch all routes to publish fresh data)
295 * Unfortunately, this function may be called several times during the "reconstruction" of the routing table.
296 */
297
Tomáš Pecka088e7742021-04-28 12:40:18 +0200298 std::vector<utils::YANGPair> values;
Tomáš Peckab29f36b2021-03-31 20:02:06 +0200299 std::vector<std::string> deletePaths;
300
301 auto routes = m_rtnetlink->getRoutes();
302 auto links = m_rtnetlink->getLinks();
303
304 // 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
305 std::map<decltype(AF_INET), unsigned> routeIdx {{AF_INET, 1}, {AF_INET6, 1}};
306
307 for (const auto& route : routes) {
308 if (rtnl_route_get_table(route.get()) != RT_TABLE_MAIN) {
309 continue;
310 }
311
312 if (rtnl_route_get_type(route.get()) != RTN_UNICAST) {
313 continue;
314 }
315
316 auto family = rtnl_route_get_family(route.get());
317 if (family != AF_INET && family != AF_INET6) {
318 continue;
319 }
320
321 auto proto = rtnl_route_get_protocol(route.get());
322 if (proto != RTPROT_KERNEL && proto != RTPROT_RA && proto != RTPROT_DHCP && proto != RTPROT_STATIC && proto != RTPROT_BOOT) {
323 std::array<char, arrlen("redirect")> buf; /* "redirect" is the longest value (libnl/lib/route/route_utils.c, init_proto_names) */
324 m_log->warn("Unimplemented routing protocol {} '{}'", proto, rtnl_route_proto2str(proto, buf.data(), buf.size()));
325 continue;
326 }
327
328 const auto ribName = family == AF_INET ? "ipv4-master"s : "ipv6-master"s;
329 const auto yangPrefix = "/ietf-routing:routing/ribs/rib[name='" + ribName + "']/routes/route["s + std::to_string(routeIdx[family]++) + "]/";
330 const auto familyYangPrefix = family == AF_INET ? "ietf-ipv4-unicast-routing"s : "ietf-ipv6-unicast-routing"s;
331
332 std::string destPrefix;
333 if (auto* addr = rtnl_route_get_dst(route.get()); addr != nullptr) {
334 if (nl_addr_iszero(addr)) {
335 destPrefix = family == AF_INET ? "0.0.0.0/0" : "::/0";
336 } else {
337 std::array<char, IPV6ADDRSTRLEN_WITH_PREFIX> data;
338 destPrefix = nl_addr2str(addr, data.data(), data.size());
339
340 // append prefix len if nl_addr2str fails to do that (when prefix length is 32 in ipv4 or 128 in ipv6)
341 if (destPrefix.find_first_of('/') == std::string::npos) {
342 destPrefix += "/" + std::to_string(nl_addr_get_prefixlen(addr));
343 }
344 }
345 }
346
Tomáš Pecka088e7742021-04-28 12:40:18 +0200347 values.emplace_back(yangPrefix + familyYangPrefix + ":destination-prefix", destPrefix);
Tomáš Peckab29f36b2021-03-31 20:02:06 +0200348
349 auto scope = rtnl_route_get_scope(route.get());
350 std::string protoStr;
351 switch (proto) {
352 case RTPROT_KERNEL:
353 protoStr = scope == RT_SCOPE_LINK ? "direct" : "static";
354 break;
355 case RTPROT_STATIC:
356 case RTPROT_BOOT:
357 protoStr = "static";
358 break;
359 case RTPROT_DHCP:
360 protoStr = "czechlight-network:dhcp";
361 break;
362 case RTPROT_RA:
363 protoStr = "czechlight-network:ra";
364 break;
365 default:
366 throw std::invalid_argument("Unexpected route protocol ("s + std::to_string(proto) + ")");
367 }
368
Tomáš Pecka088e7742021-04-28 12:40:18 +0200369 values.emplace_back(yangPrefix + "source-protocol", protoStr);
Tomáš Peckab29f36b2021-03-31 20:02:06 +0200370
371 const auto hops = rtnl_route_get_nnexthops(route.get());
372 const bool multihop = hops > 1;
373 for (auto i = 0; i < hops; i++) {
374 rtnl_nexthop* nh = rtnl_route_nexthop_n(route.get(), i);
375
376 if (nl_addr* addr = rtnl_route_nh_get_gateway(nh); addr) {
377 std::string yangKey;
378 if (!multihop) {
379 yangKey = yangPrefix + "next-hop/" + familyYangPrefix + ":next-hop-address";
380 } else {
381 yangKey = yangPrefix + "next-hop/next-hop-list/next-hop[" + std::to_string(i + 1) + "]/" + familyYangPrefix + ":address";
382 }
383
384 std::array<char, IPV6ADDRSTRLEN_WITH_PREFIX> buf;
Tomáš Pecka088e7742021-04-28 12:40:18 +0200385 values.emplace_back(yangKey, nl_addr2str(addr, buf.data(), buf.size()));
Tomáš Peckab29f36b2021-03-31 20:02:06 +0200386 }
387
388 auto if_index = rtnl_route_nh_get_ifindex(nh);
389 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()) {
390 if (char* ifname = rtnl_link_get_name(linkIt->get()); ifname) {
391 std::string yangKey;
392 if (!multihop) {
393 yangKey = yangPrefix + "next-hop/outgoing-interface";
394 } else {
395 yangKey = yangPrefix + "next-hop/next-hop-list/next-hop[" + std::to_string(i + 1) + "]/outgoing-interface";
396 }
397
Tomáš Pecka088e7742021-04-28 12:40:18 +0200398 values.emplace_back(yangKey, rtnl_link_get_name(linkIt->get()));
Tomáš Peckab29f36b2021-03-31 20:02:06 +0200399 }
400 }
401 }
402 }
403
404 utils::valuesPush(values, deletePaths, m_srSession, SR_DS_OPERATIONAL);
405}
Tomáš Peckaf10b9302021-02-23 19:02:02 +0100406}