]>
Commit | Line | Data |
---|---|---|
63df8e85 DA |
1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* | |
3 | * ip nexthop | |
4 | * | |
5 | * Copyright (c) 2017-19 David Ahern <dsahern@gmail.com> | |
6 | */ | |
7 | ||
8 | #include <linux/nexthop.h> | |
9 | #include <stdio.h> | |
10 | #include <string.h> | |
11 | #include <rt_names.h> | |
12 | #include <errno.h> | |
13 | ||
14 | #include "utils.h" | |
15 | #include "ip_common.h" | |
16 | ||
17 | static struct { | |
18 | unsigned int flushed; | |
19 | unsigned int groups; | |
20 | unsigned int ifindex; | |
21 | unsigned int master; | |
84b91683 | 22 | unsigned int proto; |
a56d1746 | 23 | unsigned int fdb; |
63df8e85 DA |
24 | } filter; |
25 | ||
26 | enum { | |
27 | IPNH_LIST, | |
28 | IPNH_FLUSH, | |
29 | }; | |
30 | ||
31 | #define RTM_NHA(h) ((struct rtattr *)(((char *)(h)) + \ | |
32 | NLMSG_ALIGN(sizeof(struct nhmsg)))) | |
33 | ||
34 | static void usage(void) __attribute__((noreturn)); | |
35 | ||
36 | static void usage(void) | |
37 | { | |
38 | fprintf(stderr, | |
84b91683 | 39 | "Usage: ip nexthop { list | flush } [ protocol ID ] SELECTOR\n" |
63df8e85 DA |
40 | " ip nexthop { add | replace } id ID NH [ protocol ID ]\n" |
41 | " ip nexthop { get| del } id ID\n" | |
42 | "SELECTOR := [ id ID ] [ dev DEV ] [ vrf NAME ] [ master DEV ]\n" | |
a56d1746 | 43 | " [ groups ] [ fdb ]\n" |
63df8e85 DA |
44 | "NH := { blackhole | [ via ADDRESS ] [ dev DEV ] [ onlink ]\n" |
45 | " [ encap ENCAPTYPE ENCAPHDR ] | group GROUP ] }\n" | |
46 | "GROUP := [ id[,weight]>/<id[,weight]>/... ]\n" | |
47 | "ENCAPTYPE := [ mpls ]\n" | |
48 | "ENCAPHDR := [ MPLSLABEL ]\n"); | |
49 | exit(-1); | |
50 | } | |
51 | ||
52 | static int nh_dump_filter(struct nlmsghdr *nlh, int reqlen) | |
53 | { | |
54 | int err; | |
55 | ||
56 | if (filter.ifindex) { | |
57 | err = addattr32(nlh, reqlen, NHA_OIF, filter.ifindex); | |
58 | if (err) | |
59 | return err; | |
60 | } | |
61 | ||
62 | if (filter.groups) { | |
d9b86843 | 63 | err = addattr_l(nlh, reqlen, NHA_GROUPS, NULL, 0); |
63df8e85 DA |
64 | if (err) |
65 | return err; | |
66 | } | |
67 | ||
68 | if (filter.master) { | |
d9b86843 | 69 | err = addattr32(nlh, reqlen, NHA_MASTER, filter.master); |
63df8e85 DA |
70 | if (err) |
71 | return err; | |
72 | } | |
73 | ||
a56d1746 RP |
74 | if (filter.fdb) { |
75 | err = addattr_l(nlh, reqlen, NHA_FDB, NULL, 0); | |
76 | if (err) | |
77 | return err; | |
78 | } | |
79 | ||
63df8e85 DA |
80 | return 0; |
81 | } | |
82 | ||
83 | static struct rtnl_handle rth_del = { .fd = -1 }; | |
84 | ||
85 | static int delete_nexthop(__u32 id) | |
86 | { | |
87 | struct { | |
88 | struct nlmsghdr n; | |
89 | struct nhmsg nhm; | |
90 | char buf[64]; | |
91 | } req = { | |
92 | .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct nhmsg)), | |
93 | .n.nlmsg_flags = NLM_F_REQUEST, | |
94 | .n.nlmsg_type = RTM_DELNEXTHOP, | |
95 | .nhm.nh_family = AF_UNSPEC, | |
96 | }; | |
97 | ||
98 | req.n.nlmsg_seq = ++rth_del.seq; | |
99 | ||
100 | addattr32(&req.n, sizeof(req), NHA_ID, id); | |
101 | ||
102 | if (rtnl_talk(&rth_del, &req.n, NULL) < 0) | |
103 | return -1; | |
104 | return 0; | |
105 | } | |
106 | ||
107 | static int flush_nexthop(struct nlmsghdr *nlh, void *arg) | |
108 | { | |
109 | struct nhmsg *nhm = NLMSG_DATA(nlh); | |
110 | struct rtattr *tb[NHA_MAX+1]; | |
111 | __u32 id = 0; | |
112 | int len; | |
113 | ||
114 | len = nlh->nlmsg_len - NLMSG_SPACE(sizeof(*nhm)); | |
115 | if (len < 0) { | |
116 | fprintf(stderr, "BUG: wrong nlmsg len %d\n", len); | |
117 | return -1; | |
118 | } | |
119 | ||
84b91683 DS |
120 | if (filter.proto && nhm->nh_protocol != filter.proto) |
121 | return 0; | |
122 | ||
63df8e85 DA |
123 | parse_rtattr(tb, NHA_MAX, RTM_NHA(nhm), len); |
124 | if (tb[NHA_ID]) | |
125 | id = rta_getattr_u32(tb[NHA_ID]); | |
126 | ||
127 | if (id && !delete_nexthop(id)) | |
128 | filter.flushed++; | |
129 | ||
130 | return 0; | |
131 | } | |
132 | ||
133 | static int ipnh_flush(unsigned int all) | |
134 | { | |
135 | int rc = -2; | |
136 | ||
137 | if (all) { | |
138 | filter.groups = 1; | |
139 | filter.ifindex = 0; | |
140 | filter.master = 0; | |
141 | } | |
142 | ||
143 | if (rtnl_open(&rth_del, 0) < 0) { | |
144 | fprintf(stderr, "Cannot open rtnetlink\n"); | |
145 | return EXIT_FAILURE; | |
146 | } | |
147 | again: | |
148 | if (rtnl_nexthopdump_req(&rth, preferred_family, nh_dump_filter) < 0) { | |
149 | perror("Cannot send dump request"); | |
150 | goto out; | |
151 | } | |
152 | ||
153 | if (rtnl_dump_filter(&rth, flush_nexthop, stdout) < 0) { | |
154 | fprintf(stderr, "Dump terminated. Failed to flush nexthops\n"); | |
155 | goto out; | |
156 | } | |
157 | ||
158 | /* if deleting all, then remove groups first */ | |
159 | if (all && filter.groups) { | |
160 | filter.groups = 0; | |
161 | goto again; | |
162 | } | |
163 | ||
164 | rc = 0; | |
165 | out: | |
166 | rtnl_close(&rth_del); | |
167 | if (!filter.flushed) | |
168 | printf("Nothing to flush\n"); | |
169 | else | |
170 | printf("Flushed %d nexthops\n", filter.flushed); | |
171 | ||
172 | return rc; | |
173 | } | |
174 | ||
175 | static void print_nh_group(FILE *fp, const struct rtattr *grps_attr) | |
176 | { | |
177 | struct nexthop_grp *nhg = RTA_DATA(grps_attr); | |
178 | int num = RTA_PAYLOAD(grps_attr) / sizeof(*nhg); | |
179 | int i; | |
180 | ||
181 | if (!num || num * sizeof(*nhg) != RTA_PAYLOAD(grps_attr)) { | |
182 | fprintf(fp, "<invalid nexthop group>"); | |
183 | return; | |
184 | } | |
185 | ||
186 | open_json_array(PRINT_JSON, "group"); | |
187 | print_string(PRINT_FP, NULL, "%s", "group "); | |
188 | for (i = 0; i < num; ++i) { | |
189 | open_json_object(NULL); | |
190 | ||
191 | if (i) | |
192 | print_string(PRINT_FP, NULL, "%s", "/"); | |
193 | ||
194 | print_uint(PRINT_ANY, "id", "%u", nhg[i].id); | |
195 | if (nhg[i].weight) | |
196 | print_uint(PRINT_ANY, "weight", ",%u", nhg[i].weight + 1); | |
197 | ||
198 | close_json_object(); | |
199 | } | |
dba639a5 | 200 | print_string(PRINT_FP, NULL, "%s", " "); |
63df8e85 DA |
201 | close_json_array(PRINT_JSON, NULL); |
202 | } | |
203 | ||
204 | int print_nexthop(struct nlmsghdr *n, void *arg) | |
205 | { | |
206 | struct nhmsg *nhm = NLMSG_DATA(n); | |
207 | struct rtattr *tb[NHA_MAX+1]; | |
208 | FILE *fp = (FILE *)arg; | |
209 | int len; | |
210 | ||
211 | SPRINT_BUF(b1); | |
212 | ||
213 | if (n->nlmsg_type != RTM_DELNEXTHOP && | |
214 | n->nlmsg_type != RTM_NEWNEXTHOP) { | |
215 | fprintf(stderr, "Not a nexthop: %08x %08x %08x\n", | |
216 | n->nlmsg_len, n->nlmsg_type, n->nlmsg_flags); | |
217 | return -1; | |
218 | } | |
219 | ||
220 | len = n->nlmsg_len - NLMSG_SPACE(sizeof(*nhm)); | |
221 | if (len < 0) { | |
222 | close_json_object(); | |
223 | fprintf(stderr, "BUG: wrong nlmsg len %d\n", len); | |
224 | return -1; | |
225 | } | |
226 | ||
84b91683 DS |
227 | if (filter.proto && filter.proto != nhm->nh_protocol) |
228 | return 0; | |
229 | ||
63df8e85 DA |
230 | parse_rtattr(tb, NHA_MAX, RTM_NHA(nhm), len); |
231 | ||
232 | open_json_object(NULL); | |
233 | ||
2c78aba2 | 234 | if (n->nlmsg_type == RTM_DELNEXTHOP) |
63df8e85 DA |
235 | print_bool(PRINT_ANY, "deleted", "Deleted ", true); |
236 | ||
237 | if (tb[NHA_ID]) | |
238 | print_uint(PRINT_ANY, "id", "id %u ", | |
239 | rta_getattr_u32(tb[NHA_ID])); | |
240 | ||
241 | if (tb[NHA_GROUP]) | |
242 | print_nh_group(fp, tb[NHA_GROUP]); | |
243 | ||
244 | if (tb[NHA_ENCAP]) | |
245 | lwt_print_encap(fp, tb[NHA_ENCAP_TYPE], tb[NHA_ENCAP]); | |
246 | ||
247 | if (tb[NHA_GATEWAY]) | |
248 | print_rta_gateway(fp, nhm->nh_family, tb[NHA_GATEWAY]); | |
249 | ||
250 | if (tb[NHA_OIF]) | |
251 | print_rta_if(fp, tb[NHA_OIF], "dev"); | |
252 | ||
253 | if (nhm->nh_scope != RT_SCOPE_UNIVERSE || show_details > 0) { | |
254 | print_string(PRINT_ANY, "scope", "scope %s ", | |
255 | rtnl_rtscope_n2a(nhm->nh_scope, b1, sizeof(b1))); | |
256 | } | |
257 | ||
258 | if (tb[NHA_BLACKHOLE]) | |
2caa8012 | 259 | print_null(PRINT_ANY, "blackhole", "blackhole ", NULL); |
63df8e85 DA |
260 | |
261 | if (nhm->nh_protocol != RTPROT_UNSPEC || show_details > 0) { | |
262 | print_string(PRINT_ANY, "protocol", "proto %s ", | |
263 | rtnl_rtprot_n2a(nhm->nh_protocol, b1, sizeof(b1))); | |
264 | } | |
265 | ||
266 | if (tb[NHA_OIF]) | |
267 | print_rt_flags(fp, nhm->nh_flags); | |
268 | ||
a56d1746 RP |
269 | if (tb[NHA_FDB]) |
270 | print_null(PRINT_ANY, "fdb", "fdb", NULL); | |
271 | ||
63df8e85 DA |
272 | print_string(PRINT_FP, NULL, "%s", "\n"); |
273 | close_json_object(); | |
274 | fflush(fp); | |
275 | ||
276 | return 0; | |
277 | } | |
278 | ||
279 | static int add_nh_group_attr(struct nlmsghdr *n, int maxlen, char *argv) | |
280 | { | |
281 | struct nexthop_grp *grps; | |
282 | int count = 0, i; | |
283 | char *sep, *wsep; | |
284 | ||
285 | if (*argv != '\0') | |
286 | count = 1; | |
287 | ||
288 | /* separator is '/' */ | |
289 | sep = strchr(argv, '/'); | |
290 | while (sep) { | |
291 | count++; | |
292 | sep = strchr(sep + 1, '/'); | |
293 | } | |
294 | ||
295 | if (count == 0) | |
296 | return -1; | |
297 | ||
298 | grps = calloc(count, sizeof(*grps)); | |
299 | if (!grps) | |
300 | return -1; | |
301 | ||
302 | for (i = 0; i < count; ++i) { | |
303 | sep = strchr(argv, '/'); | |
304 | if (sep) | |
305 | *sep = '\0'; | |
306 | ||
307 | wsep = strchr(argv, ','); | |
308 | if (wsep) | |
309 | *wsep = '\0'; | |
310 | ||
311 | if (get_unsigned(&grps[i].id, argv, 0)) | |
312 | return -1; | |
313 | if (wsep) { | |
314 | unsigned int w; | |
315 | ||
316 | wsep++; | |
317 | if (get_unsigned(&w, wsep, 0) || w == 0 || w > 256) | |
318 | invarg("\"weight\" is invalid\n", wsep); | |
319 | grps[i].weight = w - 1; | |
320 | } | |
321 | ||
322 | if (!sep) | |
323 | break; | |
324 | ||
325 | argv = sep + 1; | |
326 | } | |
327 | ||
328 | return addattr_l(n, maxlen, NHA_GROUP, grps, count * sizeof(*grps)); | |
329 | } | |
330 | ||
331 | static int ipnh_modify(int cmd, unsigned int flags, int argc, char **argv) | |
332 | { | |
333 | struct { | |
334 | struct nlmsghdr n; | |
335 | struct nhmsg nhm; | |
336 | char buf[1024]; | |
337 | } req = { | |
338 | .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct nhmsg)), | |
339 | .n.nlmsg_flags = NLM_F_REQUEST | flags, | |
340 | .n.nlmsg_type = cmd, | |
341 | .nhm.nh_family = preferred_family, | |
342 | }; | |
343 | __u32 nh_flags = 0; | |
344 | ||
345 | while (argc > 0) { | |
346 | if (!strcmp(*argv, "id")) { | |
347 | __u32 id; | |
348 | ||
349 | NEXT_ARG(); | |
350 | if (get_unsigned(&id, *argv, 0)) | |
351 | invarg("invalid id value", *argv); | |
352 | addattr32(&req.n, sizeof(req), NHA_ID, id); | |
353 | } else if (!strcmp(*argv, "dev")) { | |
354 | int ifindex; | |
355 | ||
356 | NEXT_ARG(); | |
357 | ifindex = ll_name_to_index(*argv); | |
358 | if (!ifindex) | |
359 | invarg("Device does not exist\n", *argv); | |
360 | addattr32(&req.n, sizeof(req), NHA_OIF, ifindex); | |
361 | if (req.nhm.nh_family == AF_UNSPEC) | |
362 | req.nhm.nh_family = AF_INET; | |
363 | } else if (strcmp(*argv, "via") == 0) { | |
364 | inet_prefix addr; | |
365 | int family; | |
366 | ||
367 | NEXT_ARG(); | |
368 | family = read_family(*argv); | |
369 | if (family == AF_UNSPEC) | |
370 | family = req.nhm.nh_family; | |
371 | else | |
372 | NEXT_ARG(); | |
373 | get_addr(&addr, *argv, family); | |
374 | if (req.nhm.nh_family == AF_UNSPEC) | |
375 | req.nhm.nh_family = addr.family; | |
376 | else if (req.nhm.nh_family != addr.family) | |
377 | invarg("address family mismatch\n", *argv); | |
378 | addattr_l(&req.n, sizeof(req), NHA_GATEWAY, | |
379 | &addr.data, addr.bytelen); | |
380 | } else if (strcmp(*argv, "encap") == 0) { | |
381 | char buf[1024]; | |
382 | struct rtattr *rta = (void *)buf; | |
383 | ||
384 | rta->rta_type = NHA_ENCAP; | |
385 | rta->rta_len = RTA_LENGTH(0); | |
386 | ||
387 | lwt_parse_encap(rta, sizeof(buf), &argc, &argv, | |
388 | NHA_ENCAP, NHA_ENCAP_TYPE); | |
389 | ||
390 | if (rta->rta_len > RTA_LENGTH(0)) { | |
391 | addraw_l(&req.n, 1024, RTA_DATA(rta), | |
392 | RTA_PAYLOAD(rta)); | |
393 | } | |
394 | } else if (!strcmp(*argv, "blackhole")) { | |
395 | addattr_l(&req.n, sizeof(req), NHA_BLACKHOLE, NULL, 0); | |
396 | if (req.nhm.nh_family == AF_UNSPEC) | |
397 | req.nhm.nh_family = AF_INET; | |
a56d1746 RP |
398 | } else if (!strcmp(*argv, "fdb")) { |
399 | addattr_l(&req.n, sizeof(req), NHA_FDB, NULL, 0); | |
63df8e85 DA |
400 | } else if (!strcmp(*argv, "onlink")) { |
401 | nh_flags |= RTNH_F_ONLINK; | |
402 | } else if (!strcmp(*argv, "group")) { | |
403 | NEXT_ARG(); | |
404 | ||
405 | if (add_nh_group_attr(&req.n, sizeof(req), *argv)) | |
406 | invarg("\"group\" value is invalid\n", *argv); | |
407 | } else if (matches(*argv, "protocol") == 0) { | |
408 | __u32 prot; | |
409 | ||
410 | NEXT_ARG(); | |
411 | if (rtnl_rtprot_a2n(&prot, *argv)) | |
412 | invarg("\"protocol\" value is invalid\n", *argv); | |
413 | req.nhm.nh_protocol = prot; | |
414 | } else if (strcmp(*argv, "help") == 0) { | |
415 | usage(); | |
416 | } else { | |
417 | invarg("", *argv); | |
418 | } | |
419 | argc--; argv++; | |
420 | } | |
421 | ||
422 | req.nhm.nh_flags = nh_flags; | |
423 | ||
424 | if (rtnl_talk(&rth, &req.n, NULL) < 0) | |
425 | return -2; | |
426 | ||
427 | return 0; | |
428 | } | |
429 | ||
430 | static int ipnh_get_id(__u32 id) | |
431 | { | |
432 | struct { | |
433 | struct nlmsghdr n; | |
434 | struct nhmsg nhm; | |
435 | char buf[1024]; | |
436 | } req = { | |
437 | .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct nhmsg)), | |
438 | .n.nlmsg_flags = NLM_F_REQUEST, | |
439 | .n.nlmsg_type = RTM_GETNEXTHOP, | |
440 | .nhm.nh_family = preferred_family, | |
441 | }; | |
442 | struct nlmsghdr *answer; | |
443 | ||
444 | addattr32(&req.n, sizeof(req), NHA_ID, id); | |
445 | ||
446 | if (rtnl_talk(&rth, &req.n, &answer) < 0) | |
447 | return -2; | |
448 | ||
449 | new_json_obj(json); | |
450 | ||
451 | if (print_nexthop(answer, (void *)stdout) < 0) { | |
452 | free(answer); | |
453 | return -1; | |
454 | } | |
455 | ||
456 | delete_json_obj(); | |
457 | fflush(stdout); | |
458 | ||
459 | free(answer); | |
460 | ||
461 | return 0; | |
462 | } | |
463 | ||
464 | static int ipnh_list_flush(int argc, char **argv, int action) | |
465 | { | |
466 | unsigned int all = (argc == 0); | |
467 | ||
468 | while (argc > 0) { | |
469 | if (!matches(*argv, "dev")) { | |
470 | NEXT_ARG(); | |
471 | filter.ifindex = ll_name_to_index(*argv); | |
472 | if (!filter.ifindex) | |
473 | invarg("Device does not exist\n", *argv); | |
474 | } else if (!matches(*argv, "groups")) { | |
475 | filter.groups = 1; | |
476 | } else if (!matches(*argv, "master")) { | |
477 | NEXT_ARG(); | |
478 | filter.master = ll_name_to_index(*argv); | |
479 | if (!filter.master) | |
480 | invarg("Device does not exist\n", *argv); | |
481 | } else if (matches(*argv, "vrf") == 0) { | |
482 | NEXT_ARG(); | |
483 | if (!name_is_vrf(*argv)) | |
484 | invarg("Invalid VRF\n", *argv); | |
485 | filter.master = ll_name_to_index(*argv); | |
486 | if (!filter.master) | |
487 | invarg("VRF does not exist\n", *argv); | |
488 | } else if (!strcmp(*argv, "id")) { | |
489 | __u32 id; | |
490 | ||
491 | NEXT_ARG(); | |
492 | if (get_unsigned(&id, *argv, 0)) | |
493 | invarg("invalid id value", *argv); | |
494 | return ipnh_get_id(id); | |
84b91683 DS |
495 | } else if (!matches(*argv, "protocol")) { |
496 | __u32 proto; | |
497 | ||
498 | NEXT_ARG(); | |
499 | if (get_unsigned(&proto, *argv, 0)) | |
500 | invarg("invalid protocol value", *argv); | |
501 | filter.proto = proto; | |
a56d1746 RP |
502 | } else if (!matches(*argv, "fdb")) { |
503 | filter.fdb = 1; | |
63df8e85 DA |
504 | } else if (matches(*argv, "help") == 0) { |
505 | usage(); | |
506 | } else { | |
507 | invarg("", *argv); | |
508 | } | |
509 | argc--; argv++; | |
510 | } | |
511 | ||
512 | if (action == IPNH_FLUSH) | |
513 | return ipnh_flush(all); | |
514 | ||
515 | if (rtnl_nexthopdump_req(&rth, preferred_family, nh_dump_filter) < 0) { | |
516 | perror("Cannot send dump request"); | |
517 | return -2; | |
518 | } | |
519 | ||
520 | new_json_obj(json); | |
521 | ||
522 | if (rtnl_dump_filter(&rth, print_nexthop, stdout) < 0) { | |
523 | fprintf(stderr, "Dump terminated\n"); | |
524 | return -2; | |
525 | } | |
526 | ||
527 | delete_json_obj(); | |
528 | fflush(stdout); | |
529 | ||
530 | return 0; | |
531 | } | |
532 | ||
533 | static int ipnh_get(int argc, char **argv) | |
534 | { | |
535 | __u32 id = 0; | |
536 | ||
537 | while (argc > 0) { | |
538 | if (!strcmp(*argv, "id")) { | |
539 | NEXT_ARG(); | |
540 | if (get_unsigned(&id, *argv, 0)) | |
541 | invarg("invalid id value", *argv); | |
542 | } else { | |
543 | usage(); | |
544 | } | |
545 | argc--; argv++; | |
546 | } | |
547 | ||
548 | if (!id) { | |
549 | usage(); | |
550 | return -1; | |
551 | } | |
552 | ||
553 | return ipnh_get_id(id); | |
554 | } | |
555 | ||
556 | int do_ipnh(int argc, char **argv) | |
557 | { | |
558 | if (argc < 1) | |
559 | return ipnh_list_flush(0, NULL, IPNH_LIST); | |
560 | ||
561 | if (!matches(*argv, "add")) | |
562 | return ipnh_modify(RTM_NEWNEXTHOP, NLM_F_CREATE|NLM_F_EXCL, | |
563 | argc-1, argv+1); | |
564 | if (!matches(*argv, "replace")) | |
565 | return ipnh_modify(RTM_NEWNEXTHOP, NLM_F_CREATE|NLM_F_REPLACE, | |
566 | argc-1, argv+1); | |
567 | if (!matches(*argv, "delete")) | |
568 | return ipnh_modify(RTM_DELNEXTHOP, 0, argc-1, argv+1); | |
569 | ||
570 | if (!matches(*argv, "list") || | |
571 | !matches(*argv, "show") || | |
572 | !matches(*argv, "lst")) | |
573 | return ipnh_list_flush(argc-1, argv+1, IPNH_LIST); | |
574 | ||
575 | if (!matches(*argv, "get")) | |
576 | return ipnh_get(argc-1, argv+1); | |
577 | ||
578 | if (!matches(*argv, "flush")) | |
579 | return ipnh_list_flush(argc-1, argv+1, IPNH_FLUSH); | |
580 | ||
581 | if (!matches(*argv, "help")) | |
582 | usage(); | |
583 | ||
584 | fprintf(stderr, | |
585 | "Command \"%s\" is unknown, try \"ip nexthop help\".\n", *argv); | |
586 | exit(-1); | |
587 | } |