]>
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 | 40 | " ip nexthop { add | replace } id ID NH [ protocol ID ]\n" |
043e03a3 | 41 | " ip nexthop { get | del } id ID\n" |
63df8e85 | 42 | "SELECTOR := [ id ID ] [ dev DEV ] [ vrf NAME ] [ master DEV ]\n" |
a56d1746 | 43 | " [ groups ] [ fdb ]\n" |
63df8e85 | 44 | "NH := { blackhole | [ via ADDRESS ] [ dev DEV ] [ onlink ]\n" |
043e03a3 IS |
45 | " [ encap ENCAPTYPE ENCAPHDR ] | group GROUP [ fdb ] }\n" |
46 | "GROUP := [ <id[,weight]>/<id[,weight]>/... ]\n" | |
63df8e85 DA |
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 | ||
07886789 | 266 | print_rt_flags(fp, nhm->nh_flags); |
63df8e85 | 267 | |
a56d1746 RP |
268 | if (tb[NHA_FDB]) |
269 | print_null(PRINT_ANY, "fdb", "fdb", NULL); | |
270 | ||
63df8e85 DA |
271 | print_string(PRINT_FP, NULL, "%s", "\n"); |
272 | close_json_object(); | |
273 | fflush(fp); | |
274 | ||
275 | return 0; | |
276 | } | |
277 | ||
278 | static int add_nh_group_attr(struct nlmsghdr *n, int maxlen, char *argv) | |
279 | { | |
280 | struct nexthop_grp *grps; | |
281 | int count = 0, i; | |
282 | char *sep, *wsep; | |
283 | ||
284 | if (*argv != '\0') | |
285 | count = 1; | |
286 | ||
287 | /* separator is '/' */ | |
288 | sep = strchr(argv, '/'); | |
289 | while (sep) { | |
290 | count++; | |
291 | sep = strchr(sep + 1, '/'); | |
292 | } | |
293 | ||
294 | if (count == 0) | |
295 | return -1; | |
296 | ||
297 | grps = calloc(count, sizeof(*grps)); | |
298 | if (!grps) | |
299 | return -1; | |
300 | ||
301 | for (i = 0; i < count; ++i) { | |
302 | sep = strchr(argv, '/'); | |
303 | if (sep) | |
304 | *sep = '\0'; | |
305 | ||
306 | wsep = strchr(argv, ','); | |
307 | if (wsep) | |
308 | *wsep = '\0'; | |
309 | ||
310 | if (get_unsigned(&grps[i].id, argv, 0)) | |
311 | return -1; | |
312 | if (wsep) { | |
313 | unsigned int w; | |
314 | ||
315 | wsep++; | |
316 | if (get_unsigned(&w, wsep, 0) || w == 0 || w > 256) | |
317 | invarg("\"weight\" is invalid\n", wsep); | |
318 | grps[i].weight = w - 1; | |
319 | } | |
320 | ||
321 | if (!sep) | |
322 | break; | |
323 | ||
324 | argv = sep + 1; | |
325 | } | |
326 | ||
327 | return addattr_l(n, maxlen, NHA_GROUP, grps, count * sizeof(*grps)); | |
328 | } | |
329 | ||
330 | static int ipnh_modify(int cmd, unsigned int flags, int argc, char **argv) | |
331 | { | |
332 | struct { | |
333 | struct nlmsghdr n; | |
334 | struct nhmsg nhm; | |
335 | char buf[1024]; | |
336 | } req = { | |
337 | .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct nhmsg)), | |
338 | .n.nlmsg_flags = NLM_F_REQUEST | flags, | |
339 | .n.nlmsg_type = cmd, | |
340 | .nhm.nh_family = preferred_family, | |
341 | }; | |
342 | __u32 nh_flags = 0; | |
343 | ||
344 | while (argc > 0) { | |
345 | if (!strcmp(*argv, "id")) { | |
346 | __u32 id; | |
347 | ||
348 | NEXT_ARG(); | |
349 | if (get_unsigned(&id, *argv, 0)) | |
350 | invarg("invalid id value", *argv); | |
351 | addattr32(&req.n, sizeof(req), NHA_ID, id); | |
352 | } else if (!strcmp(*argv, "dev")) { | |
353 | int ifindex; | |
354 | ||
355 | NEXT_ARG(); | |
356 | ifindex = ll_name_to_index(*argv); | |
357 | if (!ifindex) | |
358 | invarg("Device does not exist\n", *argv); | |
359 | addattr32(&req.n, sizeof(req), NHA_OIF, ifindex); | |
360 | if (req.nhm.nh_family == AF_UNSPEC) | |
361 | req.nhm.nh_family = AF_INET; | |
362 | } else if (strcmp(*argv, "via") == 0) { | |
363 | inet_prefix addr; | |
364 | int family; | |
365 | ||
366 | NEXT_ARG(); | |
367 | family = read_family(*argv); | |
368 | if (family == AF_UNSPEC) | |
369 | family = req.nhm.nh_family; | |
370 | else | |
371 | NEXT_ARG(); | |
372 | get_addr(&addr, *argv, family); | |
373 | if (req.nhm.nh_family == AF_UNSPEC) | |
374 | req.nhm.nh_family = addr.family; | |
375 | else if (req.nhm.nh_family != addr.family) | |
376 | invarg("address family mismatch\n", *argv); | |
377 | addattr_l(&req.n, sizeof(req), NHA_GATEWAY, | |
378 | &addr.data, addr.bytelen); | |
379 | } else if (strcmp(*argv, "encap") == 0) { | |
380 | char buf[1024]; | |
381 | struct rtattr *rta = (void *)buf; | |
382 | ||
383 | rta->rta_type = NHA_ENCAP; | |
384 | rta->rta_len = RTA_LENGTH(0); | |
385 | ||
386 | lwt_parse_encap(rta, sizeof(buf), &argc, &argv, | |
387 | NHA_ENCAP, NHA_ENCAP_TYPE); | |
388 | ||
389 | if (rta->rta_len > RTA_LENGTH(0)) { | |
390 | addraw_l(&req.n, 1024, RTA_DATA(rta), | |
391 | RTA_PAYLOAD(rta)); | |
392 | } | |
393 | } else if (!strcmp(*argv, "blackhole")) { | |
394 | addattr_l(&req.n, sizeof(req), NHA_BLACKHOLE, NULL, 0); | |
395 | if (req.nhm.nh_family == AF_UNSPEC) | |
396 | req.nhm.nh_family = AF_INET; | |
a56d1746 RP |
397 | } else if (!strcmp(*argv, "fdb")) { |
398 | addattr_l(&req.n, sizeof(req), NHA_FDB, NULL, 0); | |
63df8e85 DA |
399 | } else if (!strcmp(*argv, "onlink")) { |
400 | nh_flags |= RTNH_F_ONLINK; | |
401 | } else if (!strcmp(*argv, "group")) { | |
402 | NEXT_ARG(); | |
403 | ||
404 | if (add_nh_group_attr(&req.n, sizeof(req), *argv)) | |
405 | invarg("\"group\" value is invalid\n", *argv); | |
406 | } else if (matches(*argv, "protocol") == 0) { | |
407 | __u32 prot; | |
408 | ||
409 | NEXT_ARG(); | |
410 | if (rtnl_rtprot_a2n(&prot, *argv)) | |
411 | invarg("\"protocol\" value is invalid\n", *argv); | |
412 | req.nhm.nh_protocol = prot; | |
413 | } else if (strcmp(*argv, "help") == 0) { | |
414 | usage(); | |
415 | } else { | |
416 | invarg("", *argv); | |
417 | } | |
418 | argc--; argv++; | |
419 | } | |
420 | ||
421 | req.nhm.nh_flags = nh_flags; | |
422 | ||
423 | if (rtnl_talk(&rth, &req.n, NULL) < 0) | |
424 | return -2; | |
425 | ||
426 | return 0; | |
427 | } | |
428 | ||
429 | static int ipnh_get_id(__u32 id) | |
430 | { | |
431 | struct { | |
432 | struct nlmsghdr n; | |
433 | struct nhmsg nhm; | |
434 | char buf[1024]; | |
435 | } req = { | |
436 | .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct nhmsg)), | |
437 | .n.nlmsg_flags = NLM_F_REQUEST, | |
438 | .n.nlmsg_type = RTM_GETNEXTHOP, | |
439 | .nhm.nh_family = preferred_family, | |
440 | }; | |
441 | struct nlmsghdr *answer; | |
442 | ||
443 | addattr32(&req.n, sizeof(req), NHA_ID, id); | |
444 | ||
445 | if (rtnl_talk(&rth, &req.n, &answer) < 0) | |
446 | return -2; | |
447 | ||
448 | new_json_obj(json); | |
449 | ||
450 | if (print_nexthop(answer, (void *)stdout) < 0) { | |
451 | free(answer); | |
452 | return -1; | |
453 | } | |
454 | ||
455 | delete_json_obj(); | |
456 | fflush(stdout); | |
457 | ||
458 | free(answer); | |
459 | ||
460 | return 0; | |
461 | } | |
462 | ||
463 | static int ipnh_list_flush(int argc, char **argv, int action) | |
464 | { | |
465 | unsigned int all = (argc == 0); | |
466 | ||
467 | while (argc > 0) { | |
468 | if (!matches(*argv, "dev")) { | |
469 | NEXT_ARG(); | |
470 | filter.ifindex = ll_name_to_index(*argv); | |
471 | if (!filter.ifindex) | |
472 | invarg("Device does not exist\n", *argv); | |
473 | } else if (!matches(*argv, "groups")) { | |
474 | filter.groups = 1; | |
475 | } else if (!matches(*argv, "master")) { | |
476 | NEXT_ARG(); | |
477 | filter.master = ll_name_to_index(*argv); | |
478 | if (!filter.master) | |
479 | invarg("Device does not exist\n", *argv); | |
480 | } else if (matches(*argv, "vrf") == 0) { | |
481 | NEXT_ARG(); | |
482 | if (!name_is_vrf(*argv)) | |
483 | invarg("Invalid VRF\n", *argv); | |
484 | filter.master = ll_name_to_index(*argv); | |
485 | if (!filter.master) | |
486 | invarg("VRF does not exist\n", *argv); | |
487 | } else if (!strcmp(*argv, "id")) { | |
488 | __u32 id; | |
489 | ||
490 | NEXT_ARG(); | |
491 | if (get_unsigned(&id, *argv, 0)) | |
492 | invarg("invalid id value", *argv); | |
493 | return ipnh_get_id(id); | |
84b91683 DS |
494 | } else if (!matches(*argv, "protocol")) { |
495 | __u32 proto; | |
496 | ||
497 | NEXT_ARG(); | |
498 | if (get_unsigned(&proto, *argv, 0)) | |
499 | invarg("invalid protocol value", *argv); | |
500 | filter.proto = proto; | |
a56d1746 RP |
501 | } else if (!matches(*argv, "fdb")) { |
502 | filter.fdb = 1; | |
63df8e85 DA |
503 | } else if (matches(*argv, "help") == 0) { |
504 | usage(); | |
505 | } else { | |
506 | invarg("", *argv); | |
507 | } | |
508 | argc--; argv++; | |
509 | } | |
510 | ||
511 | if (action == IPNH_FLUSH) | |
512 | return ipnh_flush(all); | |
513 | ||
514 | if (rtnl_nexthopdump_req(&rth, preferred_family, nh_dump_filter) < 0) { | |
515 | perror("Cannot send dump request"); | |
516 | return -2; | |
517 | } | |
518 | ||
519 | new_json_obj(json); | |
520 | ||
521 | if (rtnl_dump_filter(&rth, print_nexthop, stdout) < 0) { | |
522 | fprintf(stderr, "Dump terminated\n"); | |
523 | return -2; | |
524 | } | |
525 | ||
526 | delete_json_obj(); | |
527 | fflush(stdout); | |
528 | ||
529 | return 0; | |
530 | } | |
531 | ||
532 | static int ipnh_get(int argc, char **argv) | |
533 | { | |
534 | __u32 id = 0; | |
535 | ||
536 | while (argc > 0) { | |
537 | if (!strcmp(*argv, "id")) { | |
538 | NEXT_ARG(); | |
539 | if (get_unsigned(&id, *argv, 0)) | |
540 | invarg("invalid id value", *argv); | |
541 | } else { | |
542 | usage(); | |
543 | } | |
544 | argc--; argv++; | |
545 | } | |
546 | ||
547 | if (!id) { | |
548 | usage(); | |
549 | return -1; | |
550 | } | |
551 | ||
552 | return ipnh_get_id(id); | |
553 | } | |
554 | ||
555 | int do_ipnh(int argc, char **argv) | |
556 | { | |
557 | if (argc < 1) | |
558 | return ipnh_list_flush(0, NULL, IPNH_LIST); | |
559 | ||
560 | if (!matches(*argv, "add")) | |
561 | return ipnh_modify(RTM_NEWNEXTHOP, NLM_F_CREATE|NLM_F_EXCL, | |
562 | argc-1, argv+1); | |
563 | if (!matches(*argv, "replace")) | |
564 | return ipnh_modify(RTM_NEWNEXTHOP, NLM_F_CREATE|NLM_F_REPLACE, | |
565 | argc-1, argv+1); | |
566 | if (!matches(*argv, "delete")) | |
567 | return ipnh_modify(RTM_DELNEXTHOP, 0, argc-1, argv+1); | |
568 | ||
569 | if (!matches(*argv, "list") || | |
570 | !matches(*argv, "show") || | |
571 | !matches(*argv, "lst")) | |
572 | return ipnh_list_flush(argc-1, argv+1, IPNH_LIST); | |
573 | ||
574 | if (!matches(*argv, "get")) | |
575 | return ipnh_get(argc-1, argv+1); | |
576 | ||
577 | if (!matches(*argv, "flush")) | |
578 | return ipnh_list_flush(argc-1, argv+1, IPNH_FLUSH); | |
579 | ||
580 | if (!matches(*argv, "help")) | |
581 | usage(); | |
582 | ||
583 | fprintf(stderr, | |
584 | "Command \"%s\" is unknown, try \"ip nexthop help\".\n", *argv); | |
585 | exit(-1); | |
586 | } |