blob: 79937d377b18f0e5039f20cc1b05356cf630c8fa [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áš Peckaf10b9302021-02-23 19:02:02 +01009#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
16using namespace std::string_literals;
17
18namespace {
19
Tomáš Peckab29f36b2021-03-31 20:02:06 +020020/** @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 */
24template <size_t N>
25inline constexpr size_t arrlen(const char (&)[N]) noexcept
26{
27 return N;
28}
29
Tomáš Peckaf10b9302021-02-23 19:02:02 +010030const auto CZECHLIGHT_NETWORK_MODULE_NAME = "czechlight-network"s;
31const auto IETF_IP_MODULE_NAME = "ietf-ip"s;
32const auto IETF_INTERFACES_MODULE_NAME = "ietf-interfaces"s;
Tomáš Pecka3611c0e2021-04-14 09:02:14 +020033const auto IETF_ROUTING_MODULE_NAME = "ietf-routing"s;
34const auto IETF_IPV4_UNICAST_ROUTING_MODULE_NAME = "ietf-ipv4-unicast-routing";
35const auto IETF_IPV6_UNICAST_ROUTING_MODULE_NAME = "ietf-ipv6-unicast-routing";
Tomáš Peckaf10b9302021-02-23 19:02:02 +010036const auto IETF_INTERFACES = "/"s + IETF_INTERFACES_MODULE_NAME + ":interfaces"s;
37
38const 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 +020039const auto IPV6ADDRSTRLEN_WITH_PREFIX = INET6_ADDRSTRLEN + 1 + 3 /* plus slash and max three-digits prefix */;
Tomáš Peckaf10b9302021-02-23 19:02:02 +010040
41std::string operStatusToString(uint8_t operStatus, velia::Log log)
42{
43 // unfortunately we can't use libnl's rtnl_link_operstate2str, because it creates different strings than the YANG model expects
44 switch (operStatus) {
45 case IF_OPER_UP:
46 return "up";
47 case IF_OPER_DOWN:
48 return "down";
49 case IF_OPER_TESTING:
50 return "testing";
51 case IF_OPER_DORMANT:
52 return "dormant";
53 case IF_OPER_NOTPRESENT:
54 return "not-present";
55 case IF_OPER_LOWERLAYERDOWN:
56 return "lower-layer-down";
57 case IF_OPER_UNKNOWN:
58 return "unknown";
59 default:
60 log->warn("Encountered unknown operational status {}, using 'unknown'", operStatus);
61 return "unknown";
62 }
63}
64
65std::string arpTypeToString(unsigned int arptype, velia::Log log)
66{
67 switch (arptype) {
68 case ARPHRD_ETHER:
69 return "iana-if-type:ethernetCsmacd";
70 case ARPHRD_LOOPBACK:
71 return "iana-if-type:softwareLoopback";
72 case ARPHRD_SIT:
73 return "iana-if-type:sixToFour";
74 default:
75 log->warn("Encountered unknown interface type {}, using 'iana-if-type:other'", arptype);
76 return "iana-if-type:other";
77 }
78}
79
80std::string nlActionToString(int action)
81{
82 switch (action) {
83 case NL_ACT_NEW:
84 return "NEW";
85 case NL_ACT_DEL:
86 return "DEL";
87 case NL_ACT_CHANGE:
88 return "CHANGE";
89 case NL_ACT_UNSPEC:
90 return "UNSPEC";
91 case NL_ACT_GET:
92 return "GET";
93 case NL_ACT_SET:
94 return "SET";
95 default:
96 return "<unknown action>";
97 }
98}
99
Tomáš Pecka09729382021-03-08 19:36:50 +0100100std::string binaddrToString(void* binaddr, int addrFamily)
101{
102 // any IPv4 address fits into a buffer allocated for an IPv6 address
103 static_assert(INET6_ADDRSTRLEN >= INET_ADDRSTRLEN);
104 std::array<char, INET6_ADDRSTRLEN> buf;
105
106 if (const char* res = inet_ntop(addrFamily, binaddr, buf.data(), buf.size()); res != nullptr) {
107 return res;
108 } else {
109 throw std::system_error {errno, std::generic_category(), "inet_ntop"};
110 }
111}
112
113std::string getIPVersion(int addrFamily)
114{
115 switch (addrFamily) {
116 case AF_INET:
117 return "ipv4";
118 case AF_INET6:
119 return "ipv6";
120 default:
121 throw std::runtime_error("Unexpected address family " + std::to_string(addrFamily));
122 }
123}
124
Tomáš Pecka3663aae2021-03-10 13:46:31 +0100125/** @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). */
126std::map<std::string, std::string> collectNeighboursIP(std::shared_ptr<velia::system::Rtnetlink> rtnetlink, int requestedAddrFamily, velia::Log log)
127{
128 std::map<std::string, std::string> values;
129
130 for (const auto& [neigh, link] : rtnetlink->getNeighbours()) {
131 if (rtnl_neigh_get_state(neigh.get()) == NUD_NOARP) {
132 continue;
133 }
134
135 auto linkName = rtnl_link_get_name(link.get());
136
137 auto ipAddr = rtnl_neigh_get_dst(neigh.get());
138 auto ipAddrFamily = nl_addr_get_family(ipAddr);
139
140 if (ipAddrFamily != requestedAddrFamily) {
141 continue;
142 }
143
144 auto ipAddress = binaddrToString(nl_addr_get_binary_addr(ipAddr), ipAddrFamily);
145
146 auto llAddr = rtnl_neigh_get_lladdr(neigh.get());
147 std::array<char, PHYS_ADDR_BUF_SIZE> llAddrBuf {};
148 if (auto llAddress = nl_addr2str(llAddr, llAddrBuf.data(), llAddrBuf.size()); llAddress != "none"s) {
149 values[IETF_INTERFACES + "/interface[name='" + linkName + "']/ietf-ip:" + getIPVersion(ipAddrFamily) + "/neighbor[ip='" + ipAddress + "']/link-layer-address"] = llAddress;
150 } else {
151 log->warn("Neighbor '{}' on link '{}' returned link layer address 'none'", ipAddress, linkName);
152 }
153 }
154
155 return values;
156}
Tomáš Peckaf10b9302021-02-23 19:02:02 +0100157}
158
159namespace velia::system {
160
161IETFInterfaces::IETFInterfaces(std::shared_ptr<::sysrepo::Session> srSess)
162 : m_srSession(std::move(srSess))
Tomáš Pecka70e54562021-03-10 12:39:03 +0100163 , m_srSubscribe(std::make_shared<::sysrepo::Subscribe>(m_srSession))
Tomáš Peckaf10b9302021-02-23 19:02:02 +0100164 , m_log(spdlog::get("system"))
Tomáš Pecka09729382021-03-08 19:36:50 +0100165 , m_rtnetlink(std::make_shared<Rtnetlink>(
166 [this](rtnl_link* link, int action) { onLinkUpdate(link, action); },
Tomáš Peckab29f36b2021-03-31 20:02:06 +0200167 [this](rtnl_addr* addr, int action) { onAddrUpdate(addr, action); },
168 [this](rtnl_route* addr, int action) { onRouteUpdate(addr, action); }))
Tomáš Peckaf10b9302021-02-23 19:02:02 +0100169{
170 utils::ensureModuleImplemented(m_srSession, IETF_INTERFACES_MODULE_NAME, "2018-02-20");
171 utils::ensureModuleImplemented(m_srSession, IETF_IP_MODULE_NAME, "2018-02-22");
Tomáš Pecka3611c0e2021-04-14 09:02:14 +0200172 utils::ensureModuleImplemented(m_srSession, IETF_ROUTING_MODULE_NAME, "2018-03-13");
173 utils::ensureModuleImplemented(m_srSession, IETF_IPV4_UNICAST_ROUTING_MODULE_NAME, "2018-03-13");
174 utils::ensureModuleImplemented(m_srSession, IETF_IPV6_UNICAST_ROUTING_MODULE_NAME, "2018-03-13");
Tomáš Peckaf10b9302021-02-23 19:02:02 +0100175 utils::ensureModuleImplemented(m_srSession, CZECHLIGHT_NETWORK_MODULE_NAME, "2021-02-22");
Tomáš Pecka3d3cf612021-03-31 19:51:31 +0200176
177 m_rtnetlink->invokeInitialCallbacks();
Tomáš Peckab29f36b2021-03-31 20:02:06 +0200178 // TODO: Implement /ietf-routing:routing/interfaces and /ietf-routing:routing/router-id
Tomáš Pecka70e54562021-03-10 12:39:03 +0100179
180 m_srSubscribe->oper_get_items_subscribe(
181 IETF_INTERFACES_MODULE_NAME.c_str(), [this](auto session, auto, auto, auto, auto, auto& parent) {
182 std::map<std::string, std::string> values;
183 for (const auto& link : m_rtnetlink->getLinks()) {
184 const auto yangPrefix = IETF_INTERFACES + "/interface[name='" + rtnl_link_get_name(link.get()) + "']/statistics";
185
186 values[yangPrefix + "/in-octets"] = std::to_string(rtnl_link_get_stat(link.get(), RTNL_LINK_RX_BYTES));
187 values[yangPrefix + "/out-octets"] = std::to_string(rtnl_link_get_stat(link.get(), RTNL_LINK_TX_BYTES));
188 values[yangPrefix + "/in-discards"] = std::to_string(rtnl_link_get_stat(link.get(), RTNL_LINK_RX_DROPPED));
189 values[yangPrefix + "/out-discards"] = std::to_string(rtnl_link_get_stat(link.get(), RTNL_LINK_TX_DROPPED));
190 values[yangPrefix + "/in-errors"] = std::to_string(rtnl_link_get_stat(link.get(), RTNL_LINK_RX_ERRORS));
191 values[yangPrefix + "/out-errors"] = std::to_string(rtnl_link_get_stat(link.get(), RTNL_LINK_TX_ERRORS));
192 }
193
194 utils::valuesToYang(values, {}, session, parent);
195 return SR_ERR_OK;
196 },
197 (IETF_INTERFACES + "/interface/statistics").c_str());
Tomáš Pecka3663aae2021-03-10 13:46:31 +0100198
199 m_srSubscribe->oper_get_items_subscribe(
200 IETF_INTERFACES_MODULE_NAME.c_str(), [this](auto session, auto, auto, auto, auto, auto& parent) {
201 utils::valuesToYang(collectNeighboursIP(m_rtnetlink, AF_INET, m_log), {}, session, parent);
202 return SR_ERR_OK;
203 },
204 (IETF_INTERFACES + "/interface/ietf-ip:ipv4/neighbor").c_str());
205
206 m_srSubscribe->oper_get_items_subscribe(
207 IETF_INTERFACES_MODULE_NAME.c_str(), [this](auto session, auto, auto, auto, auto, auto& parent) {
208 utils::valuesToYang(collectNeighboursIP(m_rtnetlink, AF_INET6, m_log), {}, session, parent);
209 return SR_ERR_OK;
210 },
211 (IETF_INTERFACES + "/interface/ietf-ip:ipv6/neighbor").c_str());
Tomáš Peckaf10b9302021-02-23 19:02:02 +0100212}
213
214void IETFInterfaces::onLinkUpdate(rtnl_link* link, int action)
215{
216 char* name = rtnl_link_get_name(link);
217 m_log->trace("Netlink update on link '{}', action {}", name, nlActionToString(action));
218
219 if (action == NL_ACT_DEL) {
Tomáš Pecka53f08ee2021-04-28 12:38:11 +0200220 utils::valuesPush(std::vector<utils::YANGPair>{}, {IETF_INTERFACES + "/interface[name='" + name + "']"}, m_srSession, SR_DS_OPERATIONAL);
Tomáš Peckaf10b9302021-02-23 19:02:02 +0100221 } else if (action == NL_ACT_CHANGE || action == NL_ACT_NEW) {
222 std::map<std::string, std::string> values;
223 std::vector<std::string> deletePaths;
224
Tomáš Peckadb084132021-03-10 08:37:10 +0100225 auto linkAddr = rtnl_link_get_addr(link);
Tomáš Peckaf10b9302021-02-23 19:02:02 +0100226 std::array<char, PHYS_ADDR_BUF_SIZE> buf;
Tomáš Peckadb084132021-03-10 08:37:10 +0100227 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 +0100228 values[IETF_INTERFACES + "/interface[name='" + name + "']/phys-address"] = physAddr;
229 } else {
230 // delete physical address from sysrepo if not provided by rtnetlink
231 // Note: During testing I have noticed that my wireless interface loses a physical address. There were several change callbacks invoked
232 // when simply bringing the interface down and up. In some of those, nl_addr2str returned "none".
233 deletePaths.push_back({IETF_INTERFACES + "/interface[name='" + name + "']/phys-address"});
234 }
235
Tomáš Pecka019748f2021-04-13 21:34:49 +0200236 values[IETF_INTERFACES + "/interface[name='" + name + "']/type"] = rtnl_link_get_family(link) == AF_BRIDGE ? "iana-if-type:bridge" : arpTypeToString(rtnl_link_get_arptype(link), m_log);
Tomáš Peckaf10b9302021-02-23 19:02:02 +0100237 values[IETF_INTERFACES + "/interface[name='" + name + "']/oper-status"] = operStatusToString(rtnl_link_get_operstate(link), m_log);
238
239 utils::valuesPush(values, deletePaths, m_srSession, SR_DS_OPERATIONAL);
240 } else {
241 m_log->warn("Unhandled cache update action {} ({})", action, nlActionToString(action));
242 }
243}
244
Tomáš Pecka09729382021-03-08 19:36:50 +0100245void IETFInterfaces::onAddrUpdate(rtnl_addr* addr, int action)
246{
247 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)); });
248
249 auto linkName = rtnl_link_get_name(link.get());
250 auto addrFamily = rtnl_addr_get_family(addr);
251 if (addrFamily != AF_INET && addrFamily != AF_INET6) {
252 return;
253 }
254
255 m_log->trace("Netlink update on address of link '{}', action {}", linkName, nlActionToString(action));
256
257 auto nlAddr = rtnl_addr_get_local(addr);
258 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)
259 std::string ipVersion = getIPVersion(addrFamily);
260
261 std::map<std::string, std::string> values;
262 std::vector<std::string> deletePaths;
263 const auto yangPrefix = IETF_INTERFACES + "/interface[name='" + linkName + "']/ietf-ip:" + ipVersion + "/address[ip='" + ipAddress + "']";
264
265 if (action == NL_ACT_DEL) {
266 deletePaths.push_back({yangPrefix});
267 } else if (action == NL_ACT_CHANGE || action == NL_ACT_NEW) {
268 values[yangPrefix + "/prefix-length"] = std::to_string(rtnl_addr_get_prefixlen(addr));
269 } else {
270 m_log->warn("Unhandled cache update action {} ({})", action, nlActionToString(action));
271 }
272
273 utils::valuesPush(values, deletePaths, m_srSession, SR_DS_OPERATIONAL);
274}
Tomáš Peckab29f36b2021-03-31 20:02:06 +0200275
276void IETFInterfaces::onRouteUpdate(rtnl_route*, int)
277{
278 /* NOTE:
279 * We don't know the position of the changed route in the list of routes
280 * Replace the whole subtree (and therefore fetch all routes to publish fresh data)
281 * Unfortunately, this function may be called several times during the "reconstruction" of the routing table.
282 */
283
284 std::map<std::string, std::string> values;
285 std::vector<std::string> deletePaths;
286
287 auto routes = m_rtnetlink->getRoutes();
288 auto links = m_rtnetlink->getLinks();
289
290 // 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
291 std::map<decltype(AF_INET), unsigned> routeIdx {{AF_INET, 1}, {AF_INET6, 1}};
292
293 for (const auto& route : routes) {
294 if (rtnl_route_get_table(route.get()) != RT_TABLE_MAIN) {
295 continue;
296 }
297
298 if (rtnl_route_get_type(route.get()) != RTN_UNICAST) {
299 continue;
300 }
301
302 auto family = rtnl_route_get_family(route.get());
303 if (family != AF_INET && family != AF_INET6) {
304 continue;
305 }
306
307 auto proto = rtnl_route_get_protocol(route.get());
308 if (proto != RTPROT_KERNEL && proto != RTPROT_RA && proto != RTPROT_DHCP && proto != RTPROT_STATIC && proto != RTPROT_BOOT) {
309 std::array<char, arrlen("redirect")> buf; /* "redirect" is the longest value (libnl/lib/route/route_utils.c, init_proto_names) */
310 m_log->warn("Unimplemented routing protocol {} '{}'", proto, rtnl_route_proto2str(proto, buf.data(), buf.size()));
311 continue;
312 }
313
314 const auto ribName = family == AF_INET ? "ipv4-master"s : "ipv6-master"s;
315 const auto yangPrefix = "/ietf-routing:routing/ribs/rib[name='" + ribName + "']/routes/route["s + std::to_string(routeIdx[family]++) + "]/";
316 const auto familyYangPrefix = family == AF_INET ? "ietf-ipv4-unicast-routing"s : "ietf-ipv6-unicast-routing"s;
317
318 std::string destPrefix;
319 if (auto* addr = rtnl_route_get_dst(route.get()); addr != nullptr) {
320 if (nl_addr_iszero(addr)) {
321 destPrefix = family == AF_INET ? "0.0.0.0/0" : "::/0";
322 } else {
323 std::array<char, IPV6ADDRSTRLEN_WITH_PREFIX> data;
324 destPrefix = nl_addr2str(addr, data.data(), data.size());
325
326 // append prefix len if nl_addr2str fails to do that (when prefix length is 32 in ipv4 or 128 in ipv6)
327 if (destPrefix.find_first_of('/') == std::string::npos) {
328 destPrefix += "/" + std::to_string(nl_addr_get_prefixlen(addr));
329 }
330 }
331 }
332
333 values[yangPrefix + familyYangPrefix + ":destination-prefix"] = destPrefix;
334
335 auto scope = rtnl_route_get_scope(route.get());
336 std::string protoStr;
337 switch (proto) {
338 case RTPROT_KERNEL:
339 protoStr = scope == RT_SCOPE_LINK ? "direct" : "static";
340 break;
341 case RTPROT_STATIC:
342 case RTPROT_BOOT:
343 protoStr = "static";
344 break;
345 case RTPROT_DHCP:
346 protoStr = "czechlight-network:dhcp";
347 break;
348 case RTPROT_RA:
349 protoStr = "czechlight-network:ra";
350 break;
351 default:
352 throw std::invalid_argument("Unexpected route protocol ("s + std::to_string(proto) + ")");
353 }
354
355 values[yangPrefix + "source-protocol"] = protoStr;
356
357 const auto hops = rtnl_route_get_nnexthops(route.get());
358 const bool multihop = hops > 1;
359 for (auto i = 0; i < hops; i++) {
360 rtnl_nexthop* nh = rtnl_route_nexthop_n(route.get(), i);
361
362 if (nl_addr* addr = rtnl_route_nh_get_gateway(nh); addr) {
363 std::string yangKey;
364 if (!multihop) {
365 yangKey = yangPrefix + "next-hop/" + familyYangPrefix + ":next-hop-address";
366 } else {
367 yangKey = yangPrefix + "next-hop/next-hop-list/next-hop[" + std::to_string(i + 1) + "]/" + familyYangPrefix + ":address";
368 }
369
370 std::array<char, IPV6ADDRSTRLEN_WITH_PREFIX> buf;
371 values[yangKey] = nl_addr2str(addr, buf.data(), buf.size());
372 }
373
374 auto if_index = rtnl_route_nh_get_ifindex(nh);
375 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()) {
376 if (char* ifname = rtnl_link_get_name(linkIt->get()); ifname) {
377 std::string yangKey;
378 if (!multihop) {
379 yangKey = yangPrefix + "next-hop/outgoing-interface";
380 } else {
381 yangKey = yangPrefix + "next-hop/next-hop-list/next-hop[" + std::to_string(i + 1) + "]/outgoing-interface";
382 }
383
384 values[yangKey] = rtnl_link_get_name(linkIt->get());
385 }
386 }
387 }
388 }
389
390 utils::valuesPush(values, deletePaths, m_srSession, SR_DS_OPERATIONAL);
391}
Tomáš Peckaf10b9302021-02-23 19:02:02 +0100392}