1 /* SPDX-License-Identifier: LGPL-2.1-or-later
2  * Copyright © 2019 VMware, Inc. */
3 
4 #include <linux/pkt_sched.h>
5 
6 #include "alloc-util.h"
7 #include "conf-parser.h"
8 #include "in-addr-util.h"
9 #include "netlink-util.h"
10 #include "networkd-link.h"
11 #include "networkd-manager.h"
12 #include "networkd-network.h"
13 #include "networkd-queue.h"
14 #include "parse-util.h"
15 #include "qdisc.h"
16 #include "set.h"
17 #include "string-util.h"
18 #include "strv.h"
19 #include "tc-util.h"
20 
21 const QDiscVTable * const qdisc_vtable[_QDISC_KIND_MAX] = {
22         [QDISC_KIND_BFIFO] = &bfifo_vtable,
23         [QDISC_KIND_CAKE] = &cake_vtable,
24         [QDISC_KIND_CODEL] = &codel_vtable,
25         [QDISC_KIND_DRR] = &drr_vtable,
26         [QDISC_KIND_ETS] = &ets_vtable,
27         [QDISC_KIND_FQ] = &fq_vtable,
28         [QDISC_KIND_FQ_CODEL] = &fq_codel_vtable,
29         [QDISC_KIND_FQ_PIE] = &fq_pie_vtable,
30         [QDISC_KIND_GRED] = &gred_vtable,
31         [QDISC_KIND_HHF] = &hhf_vtable,
32         [QDISC_KIND_HTB] = &htb_vtable,
33         [QDISC_KIND_NETEM] = &netem_vtable,
34         [QDISC_KIND_PIE] = &pie_vtable,
35         [QDISC_KIND_QFQ] = &qfq_vtable,
36         [QDISC_KIND_PFIFO] = &pfifo_vtable,
37         [QDISC_KIND_PFIFO_FAST] = &pfifo_fast_vtable,
38         [QDISC_KIND_PFIFO_HEAD_DROP] = &pfifo_head_drop_vtable,
39         [QDISC_KIND_SFB] = &sfb_vtable,
40         [QDISC_KIND_SFQ] = &sfq_vtable,
41         [QDISC_KIND_TBF] = &tbf_vtable,
42         [QDISC_KIND_TEQL] = &teql_vtable,
43 };
44 
qdisc_new(QDiscKind kind,QDisc ** ret)45 static int qdisc_new(QDiscKind kind, QDisc **ret) {
46         _cleanup_(qdisc_freep) QDisc *qdisc = NULL;
47         int r;
48 
49         if (kind == _QDISC_KIND_INVALID) {
50                 qdisc = new(QDisc, 1);
51                 if (!qdisc)
52                         return -ENOMEM;
53 
54                 *qdisc = (QDisc) {
55                         .parent = TC_H_ROOT,
56                         .kind = kind,
57                 };
58         } else {
59                 assert(kind >= 0 && kind < _QDISC_KIND_MAX);
60                 qdisc = malloc0(qdisc_vtable[kind]->object_size);
61                 if (!qdisc)
62                         return -ENOMEM;
63 
64                 qdisc->parent = TC_H_ROOT;
65                 qdisc->kind = kind;
66 
67                 if (QDISC_VTABLE(qdisc)->init) {
68                         r = QDISC_VTABLE(qdisc)->init(qdisc);
69                         if (r < 0)
70                                 return r;
71                 }
72         }
73 
74         *ret = TAKE_PTR(qdisc);
75 
76         return 0;
77 }
78 
qdisc_new_static(QDiscKind kind,Network * network,const char * filename,unsigned section_line,QDisc ** ret)79 int qdisc_new_static(QDiscKind kind, Network *network, const char *filename, unsigned section_line, QDisc **ret) {
80         _cleanup_(config_section_freep) ConfigSection *n = NULL;
81         _cleanup_(qdisc_freep) QDisc *qdisc = NULL;
82         QDisc *existing;
83         int r;
84 
85         assert(network);
86         assert(ret);
87         assert(filename);
88         assert(section_line > 0);
89 
90         r = config_section_new(filename, section_line, &n);
91         if (r < 0)
92                 return r;
93 
94         existing = hashmap_get(network->qdiscs_by_section, n);
95         if (existing) {
96                 if (existing->kind != _QDISC_KIND_INVALID &&
97                     kind != _QDISC_KIND_INVALID &&
98                     existing->kind != kind)
99                         return -EINVAL;
100 
101                 if (existing->kind == kind || kind == _QDISC_KIND_INVALID) {
102                         *ret = existing;
103                         return 0;
104                 }
105         }
106 
107         r = qdisc_new(kind, &qdisc);
108         if (r < 0)
109                 return r;
110 
111         if (existing) {
112                 qdisc->handle = existing->handle;
113                 qdisc->parent = existing->parent;
114                 qdisc->tca_kind = TAKE_PTR(existing->tca_kind);
115 
116                 qdisc_free(existing);
117         }
118 
119         qdisc->network = network;
120         qdisc->section = TAKE_PTR(n);
121         qdisc->source = NETWORK_CONFIG_SOURCE_STATIC;
122 
123         r = hashmap_ensure_put(&network->qdiscs_by_section, &config_section_hash_ops, qdisc->section, qdisc);
124         if (r < 0)
125                 return r;
126 
127         *ret = TAKE_PTR(qdisc);
128         return 0;
129 }
130 
qdisc_free(QDisc * qdisc)131 QDisc* qdisc_free(QDisc *qdisc) {
132         if (!qdisc)
133                 return NULL;
134 
135         if (qdisc->network && qdisc->section)
136                 hashmap_remove(qdisc->network->qdiscs_by_section, qdisc->section);
137 
138         config_section_free(qdisc->section);
139 
140         if (qdisc->link)
141                 set_remove(qdisc->link->qdiscs, qdisc);
142 
143         free(qdisc->tca_kind);
144         return mfree(qdisc);
145 }
146 
qdisc_get_tca_kind(const QDisc * qdisc)147 static const char *qdisc_get_tca_kind(const QDisc *qdisc) {
148         assert(qdisc);
149 
150         return (QDISC_VTABLE(qdisc) && QDISC_VTABLE(qdisc)->tca_kind) ?
151                 QDISC_VTABLE(qdisc)->tca_kind : qdisc->tca_kind;
152 }
153 
qdisc_hash_func(const QDisc * qdisc,struct siphash * state)154 static void qdisc_hash_func(const QDisc *qdisc, struct siphash *state) {
155         assert(qdisc);
156         assert(state);
157 
158         siphash24_compress(&qdisc->handle, sizeof(qdisc->handle), state);
159         siphash24_compress(&qdisc->parent, sizeof(qdisc->parent), state);
160         siphash24_compress_string(qdisc_get_tca_kind(qdisc), state);
161 }
162 
qdisc_compare_func(const QDisc * a,const QDisc * b)163 static int qdisc_compare_func(const QDisc *a, const QDisc *b) {
164         int r;
165 
166         assert(a);
167         assert(b);
168 
169         r = CMP(a->handle, b->handle);
170         if (r != 0)
171                 return r;
172 
173         r = CMP(a->parent, b->parent);
174         if (r != 0)
175                 return r;
176 
177         return strcmp_ptr(qdisc_get_tca_kind(a), qdisc_get_tca_kind(b));
178 }
179 
180 DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR(
181         qdisc_hash_ops,
182         QDisc,
183         qdisc_hash_func,
184         qdisc_compare_func,
185         qdisc_free);
186 
qdisc_get(Link * link,const QDisc * in,QDisc ** ret)187 static int qdisc_get(Link *link, const QDisc *in, QDisc **ret) {
188         QDisc *existing;
189 
190         assert(link);
191         assert(in);
192 
193         existing = set_get(link->qdiscs, in);
194         if (!existing)
195                 return -ENOENT;
196 
197         if (ret)
198                 *ret = existing;
199         return 0;
200 }
201 
qdisc_add(Link * link,QDisc * qdisc)202 static int qdisc_add(Link *link, QDisc *qdisc) {
203         int r;
204 
205         assert(link);
206         assert(qdisc);
207 
208         r = set_ensure_put(&link->qdiscs, &qdisc_hash_ops, qdisc);
209         if (r < 0)
210                 return r;
211         if (r == 0)
212                 return -EEXIST;
213 
214         qdisc->link = link;
215         return 0;
216 }
217 
qdisc_dup(const QDisc * src,QDisc ** ret)218 static int qdisc_dup(const QDisc *src, QDisc **ret) {
219         _cleanup_(qdisc_freep) QDisc *dst = NULL;
220 
221         assert(src);
222         assert(ret);
223 
224         if (QDISC_VTABLE(src))
225                 dst = memdup(src, QDISC_VTABLE(src)->object_size);
226         else
227                 dst = newdup(QDisc, src, 1);
228         if (!dst)
229                 return -ENOMEM;
230 
231         /* clear all pointers */
232         dst->network = NULL;
233         dst->section = NULL;
234         dst->link = NULL;
235         dst->tca_kind = NULL;
236 
237         if (src->tca_kind) {
238                 dst->tca_kind = strdup(src->tca_kind);
239                 if (!dst->tca_kind)
240                         return -ENOMEM;
241         }
242 
243         *ret = TAKE_PTR(dst);
244         return 0;
245 }
246 
log_qdisc_debug(QDisc * qdisc,Link * link,const char * str)247 static void log_qdisc_debug(QDisc *qdisc, Link *link, const char *str) {
248         _cleanup_free_ char *state = NULL;
249 
250         assert(qdisc);
251         assert(str);
252 
253         if (!DEBUG_LOGGING)
254                 return;
255 
256         (void) network_config_state_to_string_alloc(qdisc->state, &state);
257 
258         log_link_debug(link, "%s %s QDisc (%s): handle=%"PRIx32":%"PRIx32", parent=%"PRIx32":%"PRIx32", kind=%s",
259                        str, strna(network_config_source_to_string(qdisc->source)), strna(state),
260                        TC_H_MAJ(qdisc->handle) >> 16, TC_H_MIN(qdisc->handle),
261                        TC_H_MAJ(qdisc->parent) >> 16, TC_H_MIN(qdisc->parent),
262                        strna(qdisc_get_tca_kind(qdisc)));
263 }
264 
link_find_qdisc(Link * link,uint32_t handle,uint32_t parent,const char * kind,QDisc ** ret)265 int link_find_qdisc(Link *link, uint32_t handle, uint32_t parent, const char *kind, QDisc **ret) {
266         QDisc *qdisc;
267 
268         assert(link);
269 
270         handle = TC_H_MAJ(handle);
271 
272         SET_FOREACH(qdisc, link->qdiscs) {
273                 if (qdisc->handle != handle)
274                         continue;
275 
276                 if (qdisc->parent != parent)
277                         continue;
278 
279                 if (qdisc->source == NETWORK_CONFIG_SOURCE_FOREIGN)
280                         continue;
281 
282                 if (!qdisc_exists(qdisc))
283                         continue;
284 
285                 if (kind && !streq_ptr(kind, qdisc_get_tca_kind(qdisc)))
286                         continue;
287 
288                 if (ret)
289                         *ret = qdisc;
290                 return 0;
291         }
292 
293         return -ENOENT;
294 }
295 
qdisc_handler(sd_netlink * rtnl,sd_netlink_message * m,Request * req,Link * link,QDisc * qdisc)296 static int qdisc_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, QDisc *qdisc) {
297         int r;
298 
299         assert(m);
300         assert(link);
301 
302         r = sd_netlink_message_get_errno(m);
303         if (r < 0 && r != -EEXIST) {
304                 log_link_message_error_errno(link, m, r, "Could not set QDisc");
305                 link_enter_failed(link);
306                 return 1;
307         }
308 
309         if (link->tc_messages == 0) {
310                 log_link_debug(link, "Traffic control configured");
311                 link->tc_configured = true;
312                 link_check_ready(link);
313         }
314 
315         return 1;
316 }
317 
qdisc_configure(QDisc * qdisc,Link * link,Request * req)318 static int qdisc_configure(QDisc *qdisc, Link *link, Request *req) {
319         _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
320         int r;
321 
322         assert(qdisc);
323         assert(link);
324         assert(link->manager);
325         assert(link->manager->rtnl);
326         assert(link->ifindex > 0);
327         assert(req);
328 
329         log_qdisc_debug(qdisc, link, "Configuring");
330 
331         r = sd_rtnl_message_new_traffic_control(link->manager->rtnl, &m, RTM_NEWQDISC,
332                                                 link->ifindex, qdisc->handle, qdisc->parent);
333         if (r < 0)
334                 return r;
335 
336         r = sd_netlink_message_append_string(m, TCA_KIND, qdisc_get_tca_kind(qdisc));
337         if (r < 0)
338                 return r;
339 
340         if (QDISC_VTABLE(qdisc) && QDISC_VTABLE(qdisc)->fill_message) {
341                 r = QDISC_VTABLE(qdisc)->fill_message(link, qdisc, m);
342                 if (r < 0)
343                         return r;
344         }
345 
346         return request_call_netlink_async(link->manager->rtnl, m, req);
347 }
348 
qdisc_is_ready_to_configure(QDisc * qdisc,Link * link)349 static bool qdisc_is_ready_to_configure(QDisc *qdisc, Link *link) {
350         assert(qdisc);
351         assert(link);
352 
353         if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
354                 return false;
355 
356         if (IN_SET(qdisc->parent, TC_H_ROOT, TC_H_CLSACT)) /* TC_H_CLSACT == TC_H_INGRESS */
357                 return true;
358 
359         return link_find_tclass(link, qdisc->parent, NULL) >= 0;
360 }
361 
qdisc_process_request(Request * req,Link * link,QDisc * qdisc)362 static int qdisc_process_request(Request *req, Link *link, QDisc *qdisc) {
363         int r;
364 
365         assert(req);
366         assert(link);
367         assert(qdisc);
368 
369         if (!qdisc_is_ready_to_configure(qdisc, link))
370                 return 0;
371 
372         r = qdisc_configure(qdisc, link, req);
373         if (r < 0)
374                 return log_link_warning_errno(link, r, "Failed to configure QDisc: %m");
375 
376         qdisc_enter_configuring(qdisc);
377         return 1;
378 }
379 
link_request_qdisc(Link * link,QDisc * qdisc)380 int link_request_qdisc(Link *link, QDisc *qdisc) {
381         QDisc *existing;
382         int r;
383 
384         assert(link);
385         assert(qdisc);
386 
387         if (qdisc_get(link, qdisc, &existing) < 0) {
388                 _cleanup_(qdisc_freep) QDisc *tmp = NULL;
389 
390                 r = qdisc_dup(qdisc, &tmp);
391                 if (r < 0)
392                         return log_oom();
393 
394                 r = qdisc_add(link, tmp);
395                 if (r < 0)
396                         return log_link_warning_errno(link, r, "Failed to store QDisc: %m");
397 
398                 existing = TAKE_PTR(tmp);
399         } else
400                 existing->source = qdisc->source;
401 
402         log_qdisc_debug(existing, link, "Requesting");
403         r = link_queue_request_safe(link, REQUEST_TYPE_TC_QDISC,
404                                     existing, NULL,
405                                     qdisc_hash_func,
406                                     qdisc_compare_func,
407                                     qdisc_process_request,
408                                     &link->tc_messages,
409                                     qdisc_handler,
410                                     NULL);
411         if (r < 0)
412                 return log_link_warning_errno(link, r, "Failed to request QDisc: %m");
413         if (r == 0)
414                 return 0;
415 
416         qdisc_enter_requesting(existing);
417         return 1;
418 }
419 
manager_rtnl_process_qdisc(sd_netlink * rtnl,sd_netlink_message * message,Manager * m)420 int manager_rtnl_process_qdisc(sd_netlink *rtnl, sd_netlink_message *message, Manager *m) {
421         _cleanup_(qdisc_freep) QDisc *tmp = NULL;
422         QDisc *qdisc = NULL;
423         Link *link;
424         uint16_t type;
425         int ifindex, r;
426 
427         assert(rtnl);
428         assert(message);
429         assert(m);
430 
431         if (sd_netlink_message_is_error(message)) {
432                 r = sd_netlink_message_get_errno(message);
433                 if (r < 0)
434                         log_message_warning_errno(message, r, "rtnl: failed to receive QDisc message, ignoring");
435 
436                 return 0;
437         }
438 
439         r = sd_netlink_message_get_type(message, &type);
440         if (r < 0) {
441                 log_warning_errno(r, "rtnl: could not get message type, ignoring: %m");
442                 return 0;
443         } else if (!IN_SET(type, RTM_NEWQDISC, RTM_DELQDISC)) {
444                 log_warning("rtnl: received unexpected message type %u when processing QDisc, ignoring.", type);
445                 return 0;
446         }
447 
448         r = sd_rtnl_message_traffic_control_get_ifindex(message, &ifindex);
449         if (r < 0) {
450                 log_warning_errno(r, "rtnl: could not get ifindex from message, ignoring: %m");
451                 return 0;
452         } else if (ifindex <= 0) {
453                 log_warning("rtnl: received QDisc message with invalid ifindex %d, ignoring.", ifindex);
454                 return 0;
455         }
456 
457         if (link_get_by_index(m, ifindex, &link) < 0) {
458                 if (!m->enumerating)
459                         log_warning("rtnl: received QDisc for link '%d' we don't know about, ignoring.", ifindex);
460                 return 0;
461         }
462 
463         r = qdisc_new(_QDISC_KIND_INVALID, &tmp);
464         if (r < 0)
465                 return log_oom();
466 
467         r = sd_rtnl_message_traffic_control_get_handle(message, &tmp->handle);
468         if (r < 0) {
469                 log_link_warning_errno(link, r, "rtnl: received QDisc message without handle, ignoring: %m");
470                 return 0;
471         }
472 
473         r = sd_rtnl_message_traffic_control_get_parent(message, &tmp->parent);
474         if (r < 0) {
475                 log_link_warning_errno(link, r, "rtnl: received QDisc message without parent, ignoring: %m");
476                 return 0;
477         }
478 
479         r = sd_netlink_message_read_string_strdup(message, TCA_KIND, &tmp->tca_kind);
480         if (r < 0) {
481                 log_link_warning_errno(link, r, "rtnl: received QDisc message without kind, ignoring: %m");
482                 return 0;
483         }
484 
485         (void) qdisc_get(link, tmp, &qdisc);
486 
487         switch (type) {
488         case RTM_NEWQDISC:
489                 if (qdisc) {
490                         qdisc_enter_configured(qdisc);
491                         log_qdisc_debug(qdisc, link, "Received remembered");
492                 } else {
493                         qdisc_enter_configured(tmp);
494                         log_qdisc_debug(tmp, link, "Received new");
495 
496                         r = qdisc_add(link, tmp);
497                         if (r < 0) {
498                                 log_link_warning_errno(link, r, "Failed to remember QDisc, ignoring: %m");
499                                 return 0;
500                         }
501 
502                         qdisc = TAKE_PTR(tmp);
503                 }
504 
505                 break;
506 
507         case RTM_DELQDISC:
508                 if (qdisc) {
509                         qdisc_enter_removed(qdisc);
510                         if (qdisc->state == 0) {
511                                 log_qdisc_debug(qdisc, link, "Forgetting");
512                                 qdisc_free(qdisc);
513                         } else
514                                 log_qdisc_debug(qdisc, link, "Removed");
515                 } else
516                         log_qdisc_debug(tmp, link, "Kernel removed unknown");
517 
518                 break;
519 
520         default:
521                 assert_not_reached();
522         }
523 
524         return 1;
525 }
526 
qdisc_section_verify(QDisc * qdisc,bool * has_root,bool * has_clsact)527 static int qdisc_section_verify(QDisc *qdisc, bool *has_root, bool *has_clsact) {
528         int r;
529 
530         assert(qdisc);
531         assert(has_root);
532         assert(has_clsact);
533 
534         if (section_is_invalid(qdisc->section))
535                 return -EINVAL;
536 
537         if (QDISC_VTABLE(qdisc) && QDISC_VTABLE(qdisc)->verify) {
538                 r = QDISC_VTABLE(qdisc)->verify(qdisc);
539                 if (r < 0)
540                         return r;
541         }
542 
543         if (qdisc->parent == TC_H_ROOT) {
544                 if (*has_root)
545                         return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
546                                                  "%s: More than one root qdisc section is defined. "
547                                                  "Ignoring the qdisc section from line %u.",
548                                                  qdisc->section->filename, qdisc->section->line);
549                 *has_root = true;
550         } else if (qdisc->parent == TC_H_CLSACT) { /* TC_H_CLSACT == TC_H_INGRESS */
551                 if (*has_clsact)
552                         return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
553                                                  "%s: More than one clsact or ingress qdisc section is defined. "
554                                                  "Ignoring the qdisc section from line %u.",
555                                                  qdisc->section->filename, qdisc->section->line);
556                 *has_clsact = true;
557         }
558 
559         return 0;
560 }
561 
network_drop_invalid_qdisc(Network * network)562 void network_drop_invalid_qdisc(Network *network) {
563         bool has_root = false, has_clsact = false;
564         QDisc *qdisc;
565 
566         assert(network);
567 
568         HASHMAP_FOREACH(qdisc, network->qdiscs_by_section)
569                 if (qdisc_section_verify(qdisc, &has_root, &has_clsact) < 0)
570                         qdisc_free(qdisc);
571 }
572 
config_parse_qdisc_parent(const char * unit,const char * filename,unsigned line,const char * section,unsigned section_line,const char * lvalue,int ltype,const char * rvalue,void * data,void * userdata)573 int config_parse_qdisc_parent(
574                 const char *unit,
575                 const char *filename,
576                 unsigned line,
577                 const char *section,
578                 unsigned section_line,
579                 const char *lvalue,
580                 int ltype,
581                 const char *rvalue,
582                 void *data,
583                 void *userdata) {
584 
585         _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
586         Network *network = data;
587         int r;
588 
589         assert(filename);
590         assert(lvalue);
591         assert(rvalue);
592         assert(data);
593 
594         r = qdisc_new_static(ltype, network, filename, section_line, &qdisc);
595         if (r == -ENOMEM)
596                 return log_oom();
597         if (r < 0) {
598                 log_syntax(unit, LOG_WARNING, filename, line, r,
599                            "More than one kind of queueing discipline, ignoring assignment: %m");
600                 return 0;
601         }
602 
603         if (streq(rvalue, "root"))
604                 qdisc->parent = TC_H_ROOT;
605         else if (streq(rvalue, "clsact")) {
606                 qdisc->parent = TC_H_CLSACT;
607                 qdisc->handle = TC_H_MAKE(TC_H_CLSACT, 0);
608         } else if (streq(rvalue, "ingress")) {
609                 qdisc->parent = TC_H_INGRESS;
610                 qdisc->handle = TC_H_MAKE(TC_H_INGRESS, 0);
611         } else {
612                 r = parse_handle(rvalue, &qdisc->parent);
613                 if (r < 0) {
614                         log_syntax(unit, LOG_WARNING, filename, line, r,
615                                    "Failed to parse 'Parent=', ignoring assignment: %s",
616                                    rvalue);
617                         return 0;
618                 }
619         }
620 
621         if (STR_IN_SET(rvalue, "clsact", "ingress")) {
622                 r = free_and_strdup(&qdisc->tca_kind, rvalue);
623                 if (r < 0)
624                         return log_oom();
625         } else
626                 qdisc->tca_kind = mfree(qdisc->tca_kind);
627 
628         TAKE_PTR(qdisc);
629 
630         return 0;
631 }
632 
config_parse_qdisc_handle(const char * unit,const char * filename,unsigned line,const char * section,unsigned section_line,const char * lvalue,int ltype,const char * rvalue,void * data,void * userdata)633 int config_parse_qdisc_handle(
634                 const char *unit,
635                 const char *filename,
636                 unsigned line,
637                 const char *section,
638                 unsigned section_line,
639                 const char *lvalue,
640                 int ltype,
641                 const char *rvalue,
642                 void *data,
643                 void *userdata) {
644 
645         _cleanup_(qdisc_free_or_set_invalidp) QDisc *qdisc = NULL;
646         Network *network = data;
647         uint16_t n;
648         int r;
649 
650         assert(filename);
651         assert(lvalue);
652         assert(rvalue);
653         assert(data);
654 
655         r = qdisc_new_static(ltype, network, filename, section_line, &qdisc);
656         if (r == -ENOMEM)
657                 return log_oom();
658         if (r < 0) {
659                 log_syntax(unit, LOG_WARNING, filename, line, r,
660                            "More than one kind of queueing discipline, ignoring assignment: %m");
661                 return 0;
662         }
663 
664         if (isempty(rvalue)) {
665                 qdisc->handle = TC_H_UNSPEC;
666                 TAKE_PTR(qdisc);
667                 return 0;
668         }
669 
670         r = safe_atou16_full(rvalue, 16, &n);
671         if (r < 0) {
672                 log_syntax(unit, LOG_WARNING, filename, line, r,
673                            "Failed to parse 'Handle=', ignoring assignment: %s",
674                            rvalue);
675                 return 0;
676         }
677 
678         qdisc->handle = (uint32_t) n << 16;
679         TAKE_PTR(qdisc);
680 
681         return 0;
682 }
683