/* SPDX-License-Identifier: LGPL-2.1-or-later */ #include "netdev.h" #include "netlink-util.h" #include "networkd-link.h" #include "networkd-manager.h" #include "networkd-queue.h" #include "string-table.h" #define REPLY_CALLBACK_COUNT_THRESHOLD 128 static Request* request_detach_impl(Request *req) { assert(req); if (!req->manager) return NULL; ordered_set_remove(req->manager->request_queue, req); req->manager = NULL; return req; } void request_detach(Request *req) { request_unref(request_detach_impl(req)); } static Request *request_free(Request *req) { if (!req) return NULL; /* To prevent from triggering assertions in the hash and compare functions, remove this request * from the set before freeing userdata below. */ request_detach_impl(req); if (req->free_func) req->free_func(req->userdata); if (req->counter) (*req->counter)--; link_unref(req->link); /* link may be NULL, but link_unref() can handle it gracefully. */ return mfree(req); } DEFINE_TRIVIAL_REF_UNREF_FUNC(Request, request, request_free); static void request_destroy_callback(Request *req) { assert(req); request_detach(req); request_unref(req); } static void request_hash_func(const Request *req, struct siphash *state) { assert(req); assert(state); siphash24_compress_typesafe(req->type, state); if (!IN_SET(req->type, REQUEST_TYPE_NEXTHOP, REQUEST_TYPE_ROUTE, REQUEST_TYPE_ROUTING_POLICY_RULE)) { siphash24_compress_boolean(req->link, state); if (req->link) siphash24_compress_typesafe(req->link->ifindex, state); } siphash24_compress_typesafe(req->hash_func, state); siphash24_compress_typesafe(req->compare_func, state); if (req->hash_func) req->hash_func(req->userdata, state); } static int request_compare_func(const struct Request *a, const struct Request *b) { int r; assert(a); assert(b); r = CMP(a->type, b->type); if (r != 0) return r; if (!IN_SET(a->type, REQUEST_TYPE_NEXTHOP, REQUEST_TYPE_ROUTE, REQUEST_TYPE_ROUTING_POLICY_RULE)) { r = CMP(!!a->link, !!b->link); if (r != 0) return r; if (a->link) { r = CMP(a->link->ifindex, b->link->ifindex); if (r != 0) return r; } } r = CMP(PTR_TO_UINT64(a->hash_func), PTR_TO_UINT64(b->hash_func)); if (r != 0) return r; r = CMP(PTR_TO_UINT64(a->compare_func), PTR_TO_UINT64(b->compare_func)); if (r != 0) return r; if (a->compare_func) return a->compare_func(a->userdata, b->userdata); return 0; } DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( request_hash_ops, Request, request_hash_func, request_compare_func, request_detach); static int request_new( Manager *manager, Link *link, RequestType type, void *userdata, mfree_func_t free_func, hash_func_t hash_func, compare_func_t compare_func, request_process_func_t process, unsigned *counter, request_netlink_handler_t netlink_handler, Request **ret) { _cleanup_(request_unrefp) Request *req = NULL; Request *existing; int r; assert(manager); assert(process); /* Note, requests will be processed only when the manager is in MANAGER_RUNNING. If a new operation * is requested when the manager is in MANAGER_TERMINATING or MANAGER_RESTARTING, the request will be * successfully queued but will never be processed. Then, here why we refuse new requests when the * manager is in MANAGER_STOPPED? This is because we cannot call link_ref() in that case, as this may * be called during link_free(), that means the reference counter of the link is already 0 and * calling link_ref() below triggers assertion. */ if (manager->state == MANAGER_STOPPED) return -EBUSY; req = new(Request, 1); if (!req) return -ENOMEM; *req = (Request) { .n_ref = 1, .link = link_ref(link), /* link may be NULL, but link_ref() handles it gracefully. */ .type = type, .userdata = userdata, .hash_func = hash_func, .compare_func = compare_func, .process = process, .netlink_handler = netlink_handler, }; existing = ordered_set_get(manager->request_queue, req); if (existing) { if (ret) *ret = existing; return 0; } r = ordered_set_ensure_put(&manager->request_queue, &request_hash_ops, req); if (r < 0) return r; req->manager = manager; req->free_func = free_func; req->counter = counter; if (req->counter) (*req->counter)++; /* If this is called in the ORDERED_SET_FOREACH() loop of manager_process_requests(), we need to * exit from the loop, due to the limitation of the iteration on OrderedSet. */ manager->request_queued = true; if (ret) *ret = req; TAKE_PTR(req); return 1; } int netdev_queue_request( NetDev *netdev, request_process_func_t process, Request **ret) { int r; assert(netdev); assert(netdev->manager); r = request_new(netdev->manager, NULL, REQUEST_TYPE_NETDEV_INDEPENDENT, netdev, (mfree_func_t) netdev_unref, trivial_hash_func, trivial_compare_func, process, NULL, NULL, ret); if (r <= 0) return r; netdev_ref(netdev); return 1; } int link_queue_request_full( Link *link, RequestType type, void *userdata, mfree_func_t free_func, hash_func_t hash_func, compare_func_t compare_func, request_process_func_t process, unsigned *counter, request_netlink_handler_t netlink_handler, Request **ret) { assert(link); return request_new(link->manager, link, type, userdata, free_func, hash_func, compare_func, process, counter, netlink_handler, ret); } int manager_queue_request_full( Manager *manager, RequestType type, void *userdata, mfree_func_t free_func, hash_func_t hash_func, compare_func_t compare_func, request_process_func_t process, unsigned *counter, request_netlink_handler_t netlink_handler, Request **ret) { return request_new(manager, NULL, type, userdata, free_func, hash_func, compare_func, process, counter, netlink_handler, ret); } int link_requeue_request(Link *link, Request *req, void *userdata, Request **ret) { assert(link); assert(req); return link_queue_request_full( link, req->type, userdata, req->free_func, req->hash_func, req->compare_func, req->process, req->counter, req->netlink_handler, ret); } int manager_process_requests(Manager *manager) { Request *req; int r; assert(manager); /* Process only when no remove request is queued. */ if (!ordered_set_isempty(manager->remove_request_queue)) return 0; manager->request_queued = false; ORDERED_SET_FOREACH(req, manager->request_queue) { if (req->waiting_reply) continue; /* Already processed, and waiting for netlink reply. */ /* Typically, requests send netlink message asynchronously. If there are many requests * queued, then this event may make reply callback queue in sd-netlink full. */ if (netlink_get_reply_callback_count(manager->rtnl) >= REPLY_CALLBACK_COUNT_THRESHOLD || netlink_get_reply_callback_count(manager->genl) >= REPLY_CALLBACK_COUNT_THRESHOLD || fw_ctx_get_reply_callback_count(manager->fw_ctx) >= REPLY_CALLBACK_COUNT_THRESHOLD) break; /* Avoid the request and link freed by req->process() and request_detach(). */ _unused_ _cleanup_(request_unrefp) Request *req_unref = request_ref(req); _cleanup_(link_unrefp) Link *link = link_ref(req->link); assert(req->process); r = req->process(req, link, req->userdata); if (r < 0) { request_detach(req); if (link) { link_enter_failed(link); /* link_enter_failed() may detach multiple requests from the queue. * Hence, we need to exit from the loop. */ break; } } if (r > 0 && !req->waiting_reply) /* If the request sends netlink message, e.g. for Address or so, the Request object is * referenced by the netlink slot, and will be detached later by its destroy callback. * Otherwise, e.g. for DHCP client or so, detach the request from queue now. */ request_detach(req); if (manager->request_queued) break; /* New request is queued. Exit from the loop. */ } return 0; } static int request_netlink_handler(sd_netlink *nl, sd_netlink_message *m, Request *req) { assert(req); if (req->counter) { assert(*req->counter > 0); (*req->counter)--; req->counter = NULL; /* To prevent double decrement on free. */ } if (req->link && IN_SET(req->link->state, LINK_STATE_FAILED, LINK_STATE_LINGER)) return 0; if (req->netlink_handler) return req->netlink_handler(nl, m, req, req->link, req->userdata); return 0; } int request_call_netlink_async(sd_netlink *nl, sd_netlink_message *m, Request *req) { int r; assert(nl); assert(m); assert(req); r = netlink_call_async(nl, NULL, m, request_netlink_handler, request_destroy_callback, req); if (r < 0) return r; request_ref(req); req->waiting_reply = true; return 0; } static const char *const request_type_table[_REQUEST_TYPE_MAX] = { [REQUEST_TYPE_ACTIVATE_LINK] = "activate link", [REQUEST_TYPE_ADDRESS] = "address", [REQUEST_TYPE_ADDRESS_LABEL] = "address label", [REQUEST_TYPE_BRIDGE_FDB] = "bridge FDB", [REQUEST_TYPE_BRIDGE_MDB] = "bridge MDB", [REQUEST_TYPE_DHCP_SERVER] = "DHCP server", [REQUEST_TYPE_DHCP4_CLIENT] = "DHCPv4 client", [REQUEST_TYPE_DHCP6_CLIENT] = "DHCPv6 client", [REQUEST_TYPE_IPV6_PROXY_NDP] = "IPv6 proxy NDP", [REQUEST_TYPE_NDISC] = "NDisc", [REQUEST_TYPE_NEIGHBOR] = "neighbor", [REQUEST_TYPE_NETDEV_INDEPENDENT] = "independent netdev", [REQUEST_TYPE_NETDEV_STACKED] = "stacked netdev", [REQUEST_TYPE_NEXTHOP] = "nexthop", [REQUEST_TYPE_RADV] = "RADV", [REQUEST_TYPE_ROUTE] = "route", [REQUEST_TYPE_ROUTING_POLICY_RULE] = "routing policy rule", [REQUEST_TYPE_SET_LINK_ADDRESS_GENERATION_MODE] = "IPv6LL address generation mode", [REQUEST_TYPE_SET_LINK_BOND] = "bond configurations", [REQUEST_TYPE_SET_LINK_BRIDGE] = "bridge configurations", [REQUEST_TYPE_SET_LINK_BRIDGE_VLAN] = "bridge VLAN configurations (step 1)", [REQUEST_TYPE_DEL_LINK_BRIDGE_VLAN] = "bridge VLAN configurations (step 2)", [REQUEST_TYPE_SET_LINK_CAN] = "CAN interface configurations", [REQUEST_TYPE_SET_LINK_FLAGS] = "link flags", [REQUEST_TYPE_SET_LINK_GROUP] = "interface group", [REQUEST_TYPE_SET_LINK_IPOIB] = "IPoIB configurations", [REQUEST_TYPE_SET_LINK_MAC] = "MAC address", [REQUEST_TYPE_SET_LINK_MASTER] = "master interface", [REQUEST_TYPE_SET_LINK_MTU] = "MTU", [REQUEST_TYPE_SRIOV] = "SR-IOV", [REQUEST_TYPE_TC_QDISC] = "QDisc", [REQUEST_TYPE_TC_CLASS] = "TClass", [REQUEST_TYPE_UP_DOWN] = "bring link up or down", }; DEFINE_STRING_TABLE_LOOKUP_TO_STRING(request_type, RequestType); static RemoveRequest* remove_request_free(RemoveRequest *req) { if (!req) return NULL; if (req->manager) ordered_set_remove(req->manager->remove_request_queue, req); if (req->unref_func) req->unref_func(req->userdata); link_unref(req->link); sd_netlink_unref(req->netlink); sd_netlink_message_unref(req->message); return mfree(req); } DEFINE_TRIVIAL_CLEANUP_FUNC(RemoveRequest*, remove_request_free); DEFINE_TRIVIAL_DESTRUCTOR(remove_request_destroy_callback, RemoveRequest, remove_request_free); DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( remove_request_hash_ops, void, trivial_hash_func, trivial_compare_func, remove_request_free); int remove_request_add( Manager *manager, Link *link, void *userdata, mfree_func_t unref_func, sd_netlink *netlink, sd_netlink_message *message, remove_request_netlink_handler_t netlink_handler) { _cleanup_(remove_request_freep) RemoveRequest *req = NULL; int r; assert(manager); assert(userdata); assert(netlink); assert(message); /* Unlike request_new(), remove requests will be also processed when the manager is in * MANAGER_TERMINATING or MANAGER_RESTARTING. When the manager is in MANAGER_STOPPED, we cannot * queue new remove requests anymore with the same reason explained in request_new(). */ if (manager->state == MANAGER_STOPPED) return 0; req = new(RemoveRequest, 1); if (!req) return -ENOMEM; *req = (RemoveRequest) { .link = link_ref(link), /* link may be NULL, but link_ref() handles it gracefully. */ .userdata = userdata, .netlink = sd_netlink_ref(netlink), .message = sd_netlink_message_ref(message), .netlink_handler = netlink_handler, }; r = ordered_set_ensure_put(&manager->remove_request_queue, &remove_request_hash_ops, req); if (r < 0) return r; assert(r > 0); req->manager = manager; req->unref_func = unref_func; TAKE_PTR(req); return 0; } int manager_process_remove_requests(Manager *manager) { RemoveRequest *req; int r; assert(manager); while ((req = ordered_set_first(manager->remove_request_queue))) { /* Do not make the reply callback queue in sd-netlink full. */ if (netlink_get_reply_callback_count(req->netlink) >= REPLY_CALLBACK_COUNT_THRESHOLD) return 0; r = netlink_call_async( req->netlink, NULL, req->message, req->netlink_handler, remove_request_destroy_callback, req); if (r < 0) { _cleanup_(link_unrefp) Link *link = link_ref(req->link); log_link_warning_errno(link, r, "Failed to call netlink message: %m"); /* First free the request. */ remove_request_free(req); /* Then, make the link enter the failed state. */ if (link) link_enter_failed(link); } else { /* On success, netlink needs to be unref()ed. Otherwise, the netlink and remove * request may not freed on shutting down. */ req->netlink = sd_netlink_unref(req->netlink); ordered_set_remove(manager->remove_request_queue, req); } } return 0; }