1 | #!/usr/bin/env python3 |
---|
2 | # encoding: utf8 |
---|
3 | |
---|
4 | """ |
---|
5 | This program is taken from https://github.com/brona/iproute2mac |
---|
6 | When doing 'brew install iproute2mac', we get an old version (1.4.2) that doesn't support 'ip -json' |
---|
7 | The alternative installation method recomended in the project's README file is 'curl; chmod; mv' |
---|
8 | Therefore we make the decision of shipping this ip.py (version 1.5.0) along opengnsys, which is pretty much the same as curling it |
---|
9 | """ |
---|
10 | |
---|
11 | |
---|
12 | """ |
---|
13 | iproute2mac |
---|
14 | CLI wrapper for basic network utilites on Mac OS X. |
---|
15 | Homepage: https://github.com/brona/iproute2mac |
---|
16 | |
---|
17 | The MIT License (MIT) |
---|
18 | Copyright (c) 2015 Bronislav Robenek <brona@robenek.me> |
---|
19 | """ |
---|
20 | |
---|
21 | import ipaddress |
---|
22 | import json |
---|
23 | import os |
---|
24 | import random |
---|
25 | import re |
---|
26 | import socket |
---|
27 | import string |
---|
28 | import subprocess |
---|
29 | import sys |
---|
30 | import types |
---|
31 | |
---|
32 | # Version |
---|
33 | VERSION = "1.5.0" |
---|
34 | |
---|
35 | # Utilities |
---|
36 | SUDO = "/usr/bin/sudo" |
---|
37 | IFCONFIG = "/sbin/ifconfig" |
---|
38 | ROUTE = "/sbin/route" |
---|
39 | NETSTAT = "/usr/sbin/netstat" |
---|
40 | NDP = "/usr/sbin/ndp" |
---|
41 | ARP = "/usr/sbin/arp" |
---|
42 | NETWORKSETUP = "/usr/sbin/networksetup" |
---|
43 | |
---|
44 | |
---|
45 | # Helper functions |
---|
46 | def perror(*args): |
---|
47 | sys.stderr.write(*args) |
---|
48 | sys.stderr.write("\n") |
---|
49 | |
---|
50 | |
---|
51 | def execute_cmd(cmd): |
---|
52 | print("Executing: %s" % cmd) |
---|
53 | status, output = subprocess.getstatusoutput(cmd) |
---|
54 | if status == 0: # unix/linux commands 0 true, 1 false |
---|
55 | print(output) |
---|
56 | return True |
---|
57 | else: |
---|
58 | perror(output) |
---|
59 | return False |
---|
60 | |
---|
61 | |
---|
62 | def json_dump(data, pretty): |
---|
63 | if pretty: |
---|
64 | print(json.dumps(data, indent=4)) |
---|
65 | else: |
---|
66 | print(json.dumps(data, separators=(",", ":"))) |
---|
67 | return True |
---|
68 | |
---|
69 | # Classful to CIDR conversion with "default" being passed through |
---|
70 | def cidr_from_netstat_dst(target): |
---|
71 | if target == "default": |
---|
72 | return target |
---|
73 | |
---|
74 | dots = target.count(".") |
---|
75 | if target.find("/") == -1: |
---|
76 | addr = target |
---|
77 | netmask = (dots + 1) * 8 |
---|
78 | else: |
---|
79 | [addr, netmask] = target.split("/") |
---|
80 | |
---|
81 | addr = addr + ".0" * (3 - dots) |
---|
82 | return addr + "/" + str(netmask) |
---|
83 | |
---|
84 | |
---|
85 | # Convert hexadecimal netmask in prefix length |
---|
86 | def netmask_to_length(mask): |
---|
87 | return int(mask, 16).bit_count() |
---|
88 | |
---|
89 | |
---|
90 | def any_startswith(words, test): |
---|
91 | for word in words: |
---|
92 | if word.startswith(test): |
---|
93 | return True |
---|
94 | return False |
---|
95 | |
---|
96 | |
---|
97 | # Handles passsing return value, error messages and program exit on error |
---|
98 | def help_msg(help_func): |
---|
99 | def wrapper(func): |
---|
100 | def inner(*args, **kwargs): |
---|
101 | if not func(*args, **kwargs): |
---|
102 | specific = eval(help_func) |
---|
103 | if specific: |
---|
104 | if isinstance(specific, types.FunctionType): |
---|
105 | if args and kwargs: |
---|
106 | specific(*args, **kwargs) |
---|
107 | else: |
---|
108 | specific() |
---|
109 | return False |
---|
110 | else: |
---|
111 | raise Exception("Function expected for: " + help_func) |
---|
112 | else: |
---|
113 | raise Exception( |
---|
114 | "Function variant not defined: " + help_func |
---|
115 | ) |
---|
116 | return True |
---|
117 | |
---|
118 | return inner |
---|
119 | |
---|
120 | return wrapper |
---|
121 | |
---|
122 | |
---|
123 | # Generate random MAC address with XenSource Inc. OUI |
---|
124 | # http://www.linux-kvm.com/sites/default/files/macgen.py |
---|
125 | def randomMAC(): |
---|
126 | mac = [ |
---|
127 | 0x00, |
---|
128 | 0x16, |
---|
129 | 0x3E, |
---|
130 | random.randint(0x00, 0x7F), |
---|
131 | random.randint(0x00, 0xFF), |
---|
132 | random.randint(0x00, 0xFF), |
---|
133 | ] |
---|
134 | return ":".join(["%02x" % x for x in mac]) |
---|
135 | |
---|
136 | |
---|
137 | # Decode ifconfig output |
---|
138 | def parse_ifconfig(res, af, address): |
---|
139 | links = [] |
---|
140 | count = 1 |
---|
141 | |
---|
142 | for r in res.split("\n"): |
---|
143 | if re.match(r"^\w+:", r): |
---|
144 | if count > 1: |
---|
145 | links.append(link) |
---|
146 | (ifname, flags, mtu, ifindex) = re.findall(r"^(\w+): flags=\d+<(.*)> mtu (\d+) index (\d+)", r)[0] |
---|
147 | flags = flags.split(",") |
---|
148 | link = { |
---|
149 | "ifindex": int(ifindex), |
---|
150 | "ifname": ifname, |
---|
151 | "flags": flags, |
---|
152 | "mtu": int(mtu), |
---|
153 | "operstate": "UNKNOWN", |
---|
154 | "link_type": "unknown" |
---|
155 | } |
---|
156 | if "LOOPBACK" in flags: |
---|
157 | link["link_type"] = "loopback" |
---|
158 | link["address"] = "00:00:00:00:00:00" |
---|
159 | link["broadcast"] = "00:00:00:00:00:00" |
---|
160 | elif "POINTOPOINT" in flags: |
---|
161 | link["link_type"] = "none" |
---|
162 | count = count + 1 |
---|
163 | else: |
---|
164 | if re.match(r"^\s+ether ", r): |
---|
165 | link["link_type"] = "ether" |
---|
166 | link["address"] = re.findall(r"(\w\w:\w\w:\w\w:\w\w:\w\w:\w\w)", r)[0] |
---|
167 | link["broadcast"] = "ff:ff:ff:ff:ff:ff" |
---|
168 | elif address and re.match(r"^\s+inet ", r) and af != 6: |
---|
169 | (local, netmask) = re.findall(r"inet (\d+\.\d+\.\d+\.\d+) netmask (0x[0-9a-f]+)", r)[0] |
---|
170 | addr = { |
---|
171 | "family": "inet", |
---|
172 | "local": local, |
---|
173 | "prefixlen": netmask_to_length(netmask), |
---|
174 | } |
---|
175 | if re.match(r"^.*broadcast", r): |
---|
176 | addr["broadcast"] = re.findall(r"broadcast (\d+\.\d+\.\d+\.\d+)", r)[0] |
---|
177 | link["addr_info"] = link.get("addr_info", []) + [addr] |
---|
178 | elif address and re.match(r"^\s+inet6 ", r) and af != 4: |
---|
179 | (local, prefixlen) = re.findall(r"inet6 ((?:[a-f0-9:]+:+)+[a-f0-9]+)%*\w* +prefixlen (\d+)", r)[0] |
---|
180 | link["addr_info"] = link.get("addr_info", []) + [{ |
---|
181 | "family": "inet6", |
---|
182 | "local": local, |
---|
183 | "prefixlen": int(prefixlen) |
---|
184 | }] |
---|
185 | elif re.match(r"^\s+status: ", r): |
---|
186 | match re.findall(r"status: (\w+)", r)[0]: |
---|
187 | case "active": |
---|
188 | link["operstate"] = "UP" |
---|
189 | case "inactive": |
---|
190 | link["operstate"] = "DOWN" |
---|
191 | |
---|
192 | if count > 1: |
---|
193 | links.append(link) |
---|
194 | |
---|
195 | return links |
---|
196 | |
---|
197 | |
---|
198 | def link_addr_show(argv, af, json_print, pretty_json, address): |
---|
199 | if len(argv) > 0 and argv[0] == "dev": |
---|
200 | argv.pop(0) |
---|
201 | if len(argv) > 0: |
---|
202 | param = argv[0] |
---|
203 | else: |
---|
204 | param = "-a" |
---|
205 | |
---|
206 | status, res = subprocess.getstatusoutput( |
---|
207 | IFCONFIG + " -v " + param + " 2>/dev/null" |
---|
208 | ) |
---|
209 | if status: # unix status |
---|
210 | if res == "": |
---|
211 | perror(param + " not found") |
---|
212 | else: |
---|
213 | perror(res) |
---|
214 | return False |
---|
215 | |
---|
216 | links = parse_ifconfig(res, af, address) |
---|
217 | |
---|
218 | if json_print: |
---|
219 | return json_dump(links, pretty_json) |
---|
220 | |
---|
221 | for l in links: |
---|
222 | print("%d: %s: <%s> mtu %d status %s" % ( |
---|
223 | l["ifindex"], l["ifname"], ",".join(l["flags"]), l["mtu"], l["operstate"] |
---|
224 | )) |
---|
225 | print( |
---|
226 | " link/" + l["link_type"] + |
---|
227 | ((" " + l["address"]) if "address" in l else "") + |
---|
228 | ((" brd " + l["broadcast"]) if "broadcast" in l else "") |
---|
229 | ) |
---|
230 | for a in l.get("addr_info", []): |
---|
231 | print( |
---|
232 | " %s %s/%d" % (a["family"], a["local"], a["prefixlen"]) + |
---|
233 | ((" brd " + a["broadcast"]) if "broadcast" in a else "") |
---|
234 | ) |
---|
235 | |
---|
236 | return True |
---|
237 | |
---|
238 | |
---|
239 | # Help |
---|
240 | def do_help(argv=None, af=None, json_print=None, pretty_json=None): |
---|
241 | perror("Usage: ip [ OPTIONS ] OBJECT { COMMAND | help }") |
---|
242 | perror("where OBJECT := { link | addr | route | neigh }") |
---|
243 | perror(" OPTIONS := { -V[ersion] | -j[son] | -p[retty] |") |
---|
244 | perror(" -4 | -6 }") |
---|
245 | perror("iproute2mac") |
---|
246 | perror("Homepage: https://github.com/brona/iproute2mac") |
---|
247 | perror( |
---|
248 | "This is CLI wrapper for basic network utilities on Mac OS X" |
---|
249 | " inspired with iproute2 on Linux systems." |
---|
250 | ) |
---|
251 | perror( |
---|
252 | "Provided functionality is limited and command output is not" |
---|
253 | " fully compatible with iproute2." |
---|
254 | ) |
---|
255 | perror( |
---|
256 | "For advanced usage use netstat, ifconfig, ndp, arp, route " |
---|
257 | " and networksetup directly." |
---|
258 | ) |
---|
259 | exit(255) |
---|
260 | |
---|
261 | |
---|
262 | def do_help_route(): |
---|
263 | perror("Usage: ip route list") |
---|
264 | perror(" ip route get ADDRESS") |
---|
265 | perror(" ip route { add | del | replace } ROUTE") |
---|
266 | perror(" ip route flush cache") |
---|
267 | perror(" ip route flush table main") |
---|
268 | perror("ROUTE := NODE_SPEC [ INFO_SPEC ]") |
---|
269 | perror("NODE_SPEC := [ TYPE ] PREFIX") |
---|
270 | perror("INFO_SPEC := NH") |
---|
271 | perror("TYPE := { blackhole }") |
---|
272 | perror("NH := { via ADDRESS | gw ADDRESS | nexthop ADDRESS | dev STRING }") |
---|
273 | exit(255) |
---|
274 | |
---|
275 | |
---|
276 | def do_help_addr(): |
---|
277 | perror("Usage: ip addr show [ dev STRING ]") |
---|
278 | perror(" ip addr { add | del } PREFIX dev STRING") |
---|
279 | exit(255) |
---|
280 | |
---|
281 | |
---|
282 | def do_help_link(): |
---|
283 | perror("Usage: ip link show [ DEVICE ]") |
---|
284 | perror(" ip link set dev DEVICE") |
---|
285 | perror(" [ { up | down } ]") |
---|
286 | perror(" [ address { LLADDR | factory | random } ]") |
---|
287 | perror(" [ mtu MTU ]") |
---|
288 | exit(255) |
---|
289 | |
---|
290 | |
---|
291 | def do_help_neigh(): |
---|
292 | perror("Usage: ip neighbour show [ [ to ] PREFIX ] [ dev DEV ]") |
---|
293 | perror(" ip neighbour flush [ dev DEV ]") |
---|
294 | exit(255) |
---|
295 | |
---|
296 | |
---|
297 | # Route Module |
---|
298 | @help_msg("do_help_route") |
---|
299 | def do_route(argv, af, json_print, pretty_json): |
---|
300 | if not argv or ( |
---|
301 | any_startswith(["show", "lst", "list"], argv[0]) and len(argv) == 1 |
---|
302 | ): |
---|
303 | return do_route_list(af, json_print, pretty_json) |
---|
304 | elif "get".startswith(argv[0]) and len(argv) == 2: |
---|
305 | argv.pop(0) |
---|
306 | return do_route_get(argv, af, json_print, pretty_json) |
---|
307 | elif "add".startswith(argv[0]) and len(argv) >= 3: |
---|
308 | argv.pop(0) |
---|
309 | return do_route_add(argv, af) |
---|
310 | elif "delete".startswith(argv[0]) and len(argv) >= 2: |
---|
311 | argv.pop(0) |
---|
312 | return do_route_del(argv, af) |
---|
313 | elif "replace".startswith(argv[0]) and len(argv) >= 3: |
---|
314 | argv.pop(0) |
---|
315 | return do_route_del(argv, af) and do_route_add(argv, af) |
---|
316 | elif "flush".startswith(argv[0]) and len(argv) >= 1: |
---|
317 | argv.pop(0) |
---|
318 | return do_route_flush(argv, af) |
---|
319 | else: |
---|
320 | return False |
---|
321 | return True |
---|
322 | |
---|
323 | |
---|
324 | def do_route_list(af, json_print, pretty_json): |
---|
325 | # ip route prints IPv6 or IPv4, never both |
---|
326 | inet = "inet6" if af == 6 else "inet" |
---|
327 | status, res = subprocess.getstatusoutput( |
---|
328 | NETSTAT + " -nr -f " + inet + " 2>/dev/null" |
---|
329 | ) |
---|
330 | if status: |
---|
331 | perror(res) |
---|
332 | return False |
---|
333 | res = res.split("\n") |
---|
334 | res = res[4:] # Removes first 4 lines |
---|
335 | |
---|
336 | routes = [] |
---|
337 | |
---|
338 | for r in res: |
---|
339 | ra = r.split() |
---|
340 | target = ra[0] |
---|
341 | gw = ra[1] |
---|
342 | flags = ra[2] |
---|
343 | # macOS Mojave and earlier vs Catalina |
---|
344 | dev = ra[5] if len(ra) >= 6 else ra[3] |
---|
345 | if flags.find("W") != -1: |
---|
346 | continue |
---|
347 | if af == 6: |
---|
348 | target = re.sub(r"%[^ ]+/", "/", target) |
---|
349 | else: |
---|
350 | target = cidr_from_netstat_dst(target) |
---|
351 | if flags.find("B") != -1: |
---|
352 | routes.append({"type": "blackhole", "dst": target, "flags": []}) |
---|
353 | continue |
---|
354 | if re.match(r"link.+", gw): |
---|
355 | routes.append({"dst": target, "dev": dev, "scope": "link", "flags": []}) |
---|
356 | else: |
---|
357 | routes.append({"dst": target, "gateway": gw, "dev": dev, "flags": []}) |
---|
358 | |
---|
359 | if json_print: |
---|
360 | return json_dump(routes, pretty_json) |
---|
361 | |
---|
362 | for route in routes: |
---|
363 | if "type" in route: |
---|
364 | print("%s %s" % (route["type"], route["dst"])) |
---|
365 | elif "scope" in route: |
---|
366 | print("%s dev %s scope %s" % (route["dst"], route["dev"], route["scope"])) |
---|
367 | elif "gateway" in route: |
---|
368 | print("%s via %s dev %s" % (route["dst"], route["gateway"], route["dev"])) |
---|
369 | |
---|
370 | return True |
---|
371 | |
---|
372 | |
---|
373 | def do_route_add(argv, af): |
---|
374 | options = "" |
---|
375 | if argv[0] == "blackhole": |
---|
376 | argv.pop(0) |
---|
377 | if len(argv) != 1: |
---|
378 | return False |
---|
379 | argv.append("via") |
---|
380 | argv.append("::1" if ":" in argv[0] or af == 6 else "127.0.0.1") |
---|
381 | options = "-blackhole" |
---|
382 | |
---|
383 | if len(argv) not in (3, 5): |
---|
384 | return False |
---|
385 | |
---|
386 | if len(argv) == 5: |
---|
387 | perror( |
---|
388 | "iproute2mac: Ignoring last 2 arguments, not implemented: {} {}".format( |
---|
389 | argv[3], argv[4] |
---|
390 | ) |
---|
391 | ) |
---|
392 | |
---|
393 | if argv[1] in ["via", "nexthop", "gw"]: |
---|
394 | gw = argv[2] |
---|
395 | elif argv[1] in ["dev"]: |
---|
396 | gw = "-interface " + argv[2] |
---|
397 | else: |
---|
398 | do_help_route() |
---|
399 | |
---|
400 | prefix = argv[0] |
---|
401 | inet = "-inet6 " if ":" in prefix or af == 6 else "" |
---|
402 | |
---|
403 | return execute_cmd( |
---|
404 | SUDO + " " + ROUTE + " add " + inet + prefix + " " + gw + " " + options |
---|
405 | ) |
---|
406 | |
---|
407 | |
---|
408 | def do_route_del(argv, af): |
---|
409 | options = "" |
---|
410 | if argv[0] == "blackhole": |
---|
411 | argv.pop(0) |
---|
412 | if len(argv) != 1: |
---|
413 | return False |
---|
414 | if ":" in argv[0] or af == 6: |
---|
415 | options = " ::1 -blackhole" |
---|
416 | else: |
---|
417 | options = " 127.0.0.1 -blackhole" |
---|
418 | |
---|
419 | prefix = argv[0] |
---|
420 | inet = "-inet6 " if ":" in prefix or af == 6 else "" |
---|
421 | return execute_cmd( |
---|
422 | SUDO + " " + ROUTE + " delete " + inet + prefix + options |
---|
423 | ) |
---|
424 | |
---|
425 | |
---|
426 | def do_route_flush(argv, af): |
---|
427 | if not argv: |
---|
428 | perror('"ip route flush" requires arguments.') |
---|
429 | perror("") |
---|
430 | return False |
---|
431 | |
---|
432 | # https://github.com/brona/iproute2mac/issues/38 |
---|
433 | # http://linux-ip.net/html/tools-ip-route.html |
---|
434 | if argv[0] == "cache": |
---|
435 | print("iproute2mac: There is no route cache to flush in MacOS,") |
---|
436 | print(" returning 0 status code for compatibility.") |
---|
437 | return True |
---|
438 | elif len(argv) == 2 and argv[0] == "table" and argv[1] == "main": |
---|
439 | family = "-inet6" if af == 6 else "-inet" |
---|
440 | print("iproute2mac: Flushing all routes") |
---|
441 | return execute_cmd(SUDO + " " + ROUTE + " -n flush " + family) |
---|
442 | else: |
---|
443 | return False |
---|
444 | |
---|
445 | |
---|
446 | def do_route_get(argv, af, json_print, pretty_json): |
---|
447 | target = argv[0] |
---|
448 | |
---|
449 | inet = "" |
---|
450 | if ":" in target or af == 6: |
---|
451 | inet = "-inet6 " |
---|
452 | family = socket.AF_INET6 |
---|
453 | else: |
---|
454 | family = socket.AF_INET |
---|
455 | |
---|
456 | status, res = subprocess.getstatusoutput( |
---|
457 | ROUTE + " -n get " + inet + target |
---|
458 | ) |
---|
459 | if status: # unix status or not in table |
---|
460 | perror(res) |
---|
461 | return False |
---|
462 | if res.find("not in table") >= 0: |
---|
463 | perror(res) |
---|
464 | exit(1) |
---|
465 | |
---|
466 | res = dict( |
---|
467 | re.findall( |
---|
468 | r"^\W*((?:route to|destination|gateway|interface)): (.+)$", |
---|
469 | res, |
---|
470 | re.MULTILINE, |
---|
471 | ) |
---|
472 | ) |
---|
473 | |
---|
474 | route = {"dst": res["route to"], "dev": res["interface"]} |
---|
475 | |
---|
476 | if "gateway" in res: |
---|
477 | route["gateway"] = res["gateway"] |
---|
478 | |
---|
479 | try: |
---|
480 | s = socket.socket(family, socket.SOCK_DGRAM) |
---|
481 | s.connect((route["dst"], 7)) |
---|
482 | route["prefsrc"] = src_ip = s.getsockname()[0] |
---|
483 | s.close() |
---|
484 | except: |
---|
485 | pass |
---|
486 | |
---|
487 | route["flags"] = [] |
---|
488 | route["uid"] = os.getuid() |
---|
489 | route["cache"] = [] |
---|
490 | |
---|
491 | if json_print: |
---|
492 | return json_dump([route], pretty_json) |
---|
493 | |
---|
494 | print( |
---|
495 | route["dst"] + |
---|
496 | ((" via " + route["gateway"]) if "gateway" in route else "") + |
---|
497 | " dev " + route["dev"] + |
---|
498 | ((" src " + route["prefsrc"]) if "prefsrc" in route else "") + |
---|
499 | " uid " + str(route["uid"]) |
---|
500 | ) |
---|
501 | |
---|
502 | return True |
---|
503 | |
---|
504 | |
---|
505 | # Addr Module |
---|
506 | @help_msg("do_help_addr") |
---|
507 | def do_addr(argv, af, json_print, pretty_json): |
---|
508 | if not argv: |
---|
509 | argv.append("show") |
---|
510 | |
---|
511 | if any_startswith(["show", "lst", "list"], argv[0]): |
---|
512 | argv.pop(0) |
---|
513 | return do_addr_show(argv, af, json_print, pretty_json) |
---|
514 | elif "add".startswith(argv[0]) and len(argv) >= 3: |
---|
515 | argv.pop(0) |
---|
516 | return do_addr_add(argv, af) |
---|
517 | elif "delete".startswith(argv[0]) and len(argv) >= 3: |
---|
518 | argv.pop(0) |
---|
519 | return do_addr_del(argv, af) |
---|
520 | else: |
---|
521 | return False |
---|
522 | return True |
---|
523 | |
---|
524 | |
---|
525 | def do_addr_show(argv, af, json_print, pretty_json): |
---|
526 | return link_addr_show(argv, af, json_print, pretty_json, True) |
---|
527 | |
---|
528 | |
---|
529 | def do_addr_add(argv, af): |
---|
530 | if len(argv) < 2: |
---|
531 | return False |
---|
532 | |
---|
533 | dst = "" |
---|
534 | if argv[1] == "peer": |
---|
535 | argv.pop(1) |
---|
536 | dst = argv.pop(1) |
---|
537 | |
---|
538 | if argv[1] == "dev": |
---|
539 | argv.pop(1) |
---|
540 | else: |
---|
541 | return False |
---|
542 | try: |
---|
543 | addr = argv[0] |
---|
544 | dev = argv[1] |
---|
545 | except IndexError: |
---|
546 | perror("dev not found") |
---|
547 | exit(1) |
---|
548 | inet = "" |
---|
549 | if ":" in addr or af == 6: |
---|
550 | af = 6 |
---|
551 | inet = " inet6" |
---|
552 | return execute_cmd( |
---|
553 | SUDO + " " + IFCONFIG + " " + dev + inet + " add " + addr + " " + dst |
---|
554 | ) |
---|
555 | |
---|
556 | |
---|
557 | def do_addr_del(argv, af): |
---|
558 | if len(argv) < 2: |
---|
559 | return False |
---|
560 | if argv[1] == "dev": |
---|
561 | argv.pop(1) |
---|
562 | try: |
---|
563 | addr = argv[0] |
---|
564 | dev = argv[1] |
---|
565 | except IndexError: |
---|
566 | perror("dev not found") |
---|
567 | exit(1) |
---|
568 | inet = "inet" |
---|
569 | if ":" in addr or af == 6: |
---|
570 | af = 6 |
---|
571 | inet = "inet6" |
---|
572 | return execute_cmd( |
---|
573 | SUDO + " " + IFCONFIG + " " + dev + " " + inet + " " + addr + " remove" |
---|
574 | ) |
---|
575 | |
---|
576 | |
---|
577 | # Link module |
---|
578 | @help_msg("do_help_link") |
---|
579 | def do_link(argv, af, json_print, pretty_json): |
---|
580 | if not argv: |
---|
581 | argv.append("show") |
---|
582 | |
---|
583 | if any_startswith(["show", "lst", "list"], argv[0]): |
---|
584 | argv.pop(0) |
---|
585 | return do_link_show(argv, af, json_print, pretty_json) |
---|
586 | elif "set".startswith(argv[0]): |
---|
587 | argv.pop(0) |
---|
588 | return do_link_set(argv, af) |
---|
589 | else: |
---|
590 | return False |
---|
591 | return True |
---|
592 | |
---|
593 | |
---|
594 | def do_link_show(argv, af, json_print, pretty_json): |
---|
595 | return link_addr_show(argv, af, json_print, pretty_json, False) |
---|
596 | |
---|
597 | |
---|
598 | def do_link_set(argv, af): |
---|
599 | if not argv: |
---|
600 | return False |
---|
601 | elif argv[0] == "dev": |
---|
602 | argv.pop(0) |
---|
603 | |
---|
604 | if len(argv) < 2: |
---|
605 | return False |
---|
606 | |
---|
607 | dev = argv[0] |
---|
608 | |
---|
609 | IFCONFIG_DEV_CMD = SUDO + " " + IFCONFIG + " " + dev |
---|
610 | try: |
---|
611 | args = iter(argv) |
---|
612 | for arg in args: |
---|
613 | if arg == "up": |
---|
614 | if not execute_cmd(IFCONFIG_DEV_CMD + " up"): |
---|
615 | return False |
---|
616 | elif arg == "down": |
---|
617 | if not execute_cmd(IFCONFIG_DEV_CMD + " down"): |
---|
618 | return False |
---|
619 | elif arg in ["address", "addr", "lladdr"]: |
---|
620 | addr = next(args) |
---|
621 | if addr in ["random", "rand"]: |
---|
622 | addr = randomMAC() |
---|
623 | elif addr == "factory": |
---|
624 | (status, res) = subprocess.getstatusoutput( |
---|
625 | NETWORKSETUP + " -listallhardwareports" |
---|
626 | ) |
---|
627 | if status != 0: |
---|
628 | return False |
---|
629 | details = re.findall( |
---|
630 | r"^(?:Device|Ethernet Address): (.+)$", |
---|
631 | res, |
---|
632 | re.MULTILINE, |
---|
633 | ) |
---|
634 | addr = details[details.index(dev) + 1] |
---|
635 | if not execute_cmd(IFCONFIG_DEV_CMD + " lladdr " + addr): |
---|
636 | return False |
---|
637 | elif arg == "mtu": |
---|
638 | mtu = int(next(args)) |
---|
639 | if not execute_cmd(IFCONFIG_DEV_CMD + " mtu " + str(mtu)): |
---|
640 | return False |
---|
641 | except Exception: |
---|
642 | return False |
---|
643 | return True |
---|
644 | |
---|
645 | |
---|
646 | # Neigh module |
---|
647 | @help_msg("do_help_neigh") |
---|
648 | def do_neigh(argv, af, json_print, pretty_json): |
---|
649 | if not argv: |
---|
650 | argv.append("show") |
---|
651 | |
---|
652 | if any_startswith(["show", "list", "lst"], argv[0]) and len(argv) <= 5: |
---|
653 | argv.pop(0) |
---|
654 | return do_neigh_show(argv, af, json_print, pretty_json) |
---|
655 | elif "flush".startswith(argv[0]): |
---|
656 | argv.pop(0) |
---|
657 | return do_neigh_flush(argv, af) |
---|
658 | else: |
---|
659 | return False |
---|
660 | |
---|
661 | |
---|
662 | def do_neigh_show(argv, af, json_print, pretty_json): |
---|
663 | prefix = None |
---|
664 | dev = None |
---|
665 | try: |
---|
666 | while argv: |
---|
667 | arg = argv.pop(0) |
---|
668 | if arg == "to": |
---|
669 | prefix = argv.pop(0) |
---|
670 | elif arg == "dev": |
---|
671 | dev = argv.pop(0) |
---|
672 | elif prefix is None: |
---|
673 | prefix = arg |
---|
674 | else: |
---|
675 | return False |
---|
676 | if prefix: |
---|
677 | prefix = ipaddress.ip_network(prefix, strict=False) |
---|
678 | except Exception: |
---|
679 | return False |
---|
680 | |
---|
681 | nd_ll_states = { |
---|
682 | "R": "REACHABLE", |
---|
683 | "S": "STALE", |
---|
684 | "D": "DELAY", |
---|
685 | "P": "PROBE", |
---|
686 | "I": "INCOMPLETE", |
---|
687 | "N": "INCOMPLETE", |
---|
688 | "W": "INCOMPLETE", |
---|
689 | } |
---|
690 | |
---|
691 | neighs = [] |
---|
692 | |
---|
693 | if af != 4: |
---|
694 | res = subprocess.run( |
---|
695 | [NDP, "-an"], capture_output=True, text=True, check=True |
---|
696 | ) |
---|
697 | for row in res.stdout.splitlines()[1:]: |
---|
698 | cols = row.split() |
---|
699 | entry = {"dst": re.sub(r"%.+$", "", cols[0])} |
---|
700 | if cols[1] != "(incomplete)": |
---|
701 | entry["lladdr"] = cols[1] |
---|
702 | entry["dev"] = cols[2] |
---|
703 | if dev and entry["dev"] != dev: |
---|
704 | continue |
---|
705 | if prefix and ipaddress.ip_address(entry["dst"]) not in prefix: |
---|
706 | continue |
---|
707 | if cols[1] == "(incomplete)" and cols[4] != "R": |
---|
708 | entry["status"] = ["INCOMPLETE"] |
---|
709 | else: |
---|
710 | entry["status"] = [nd_ll_states[cols[4]]] |
---|
711 | entry["router"] = len(cols) >= 6 and cols[5] == "R" |
---|
712 | neighs.append(entry) |
---|
713 | |
---|
714 | if af != 6: |
---|
715 | args = [ARP, "-anl"] |
---|
716 | if dev: |
---|
717 | args += ["-i", dev] |
---|
718 | |
---|
719 | res = subprocess.run(args, capture_output=True, text=True, check=True) |
---|
720 | for row in res.stdout.splitlines()[1:]: |
---|
721 | cols = row.split() |
---|
722 | entry = {"dst": cols[0]} |
---|
723 | if cols[1] != "(incomplete)": |
---|
724 | entry["lladdr"] = cols[1] |
---|
725 | entry["dev"] = cols[4] |
---|
726 | if dev and entry["dev"] != dev: |
---|
727 | continue |
---|
728 | if prefix and ipaddress.ip_address(entry["dst"]) not in prefix: |
---|
729 | continue |
---|
730 | if cols[1] == "(incomplete)": |
---|
731 | entry["status"] = ["INCOMPLETE"] |
---|
732 | else: |
---|
733 | entry["status"] = ["REACHABLE"] |
---|
734 | entry["router"] = False |
---|
735 | neighs.append(entry) |
---|
736 | |
---|
737 | if json_print: |
---|
738 | return json_dump(neighs, pretty_json) |
---|
739 | |
---|
740 | for nb in neighs: |
---|
741 | print( |
---|
742 | nb["dst"] + |
---|
743 | ("", " dev " + nb["dev"], "")[dev == None] + |
---|
744 | ("", " router")[nb["router"]] + |
---|
745 | " %s" % (nb["status"][0]) |
---|
746 | ) |
---|
747 | |
---|
748 | return True |
---|
749 | |
---|
750 | |
---|
751 | def do_neigh_flush(argv, af): |
---|
752 | if len(argv) != 2: |
---|
753 | perror("Flush requires arguments.") |
---|
754 | exit(1) |
---|
755 | |
---|
756 | if argv[0] != "dev": |
---|
757 | return False |
---|
758 | dev = argv[1] |
---|
759 | |
---|
760 | if af != 4: |
---|
761 | print( |
---|
762 | "iproute2mac: NDP doesn't support filtering by interface," |
---|
763 | "flushing all IPv6 entries." |
---|
764 | ) |
---|
765 | execute_cmd(SUDO + " " + NDP + " -cn") |
---|
766 | if af != 6: |
---|
767 | execute_cmd(SUDO + " " + ARP + " -a -d -i " + dev) |
---|
768 | return True |
---|
769 | |
---|
770 | |
---|
771 | # Match iproute2 commands |
---|
772 | # https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/ip.c#n86 |
---|
773 | cmds = [ |
---|
774 | ("address", do_addr), |
---|
775 | ("route", do_route), |
---|
776 | ("neighbor", do_neigh), |
---|
777 | ("neighbour", do_neigh), |
---|
778 | ("link", do_link), |
---|
779 | ("help", do_help), |
---|
780 | ] |
---|
781 | |
---|
782 | |
---|
783 | @help_msg("do_help") |
---|
784 | def main(argv): |
---|
785 | af = -1 # default / both |
---|
786 | json_print = False |
---|
787 | pretty_json = False |
---|
788 | |
---|
789 | while argv and argv[0].startswith("-"): |
---|
790 | # Turn --opt into -opt |
---|
791 | argv[0] = argv[0][1:] if argv[0][1] == "-" else argv[0] |
---|
792 | # Process options |
---|
793 | if argv[0] == "-6": |
---|
794 | af = 6 |
---|
795 | argv.pop(0) |
---|
796 | elif argv[0] == "-4": |
---|
797 | af = 4 |
---|
798 | argv.pop(0) |
---|
799 | elif argv[0].startswith("-color"): |
---|
800 | perror("iproute2mac: Color option is not implemented") |
---|
801 | argv.pop(0) |
---|
802 | elif "-json".startswith(argv[0]): |
---|
803 | json_print = True |
---|
804 | argv.pop(0) |
---|
805 | elif "-pretty".startswith(argv[0]): |
---|
806 | pretty_json = True |
---|
807 | argv.pop(0) |
---|
808 | elif "-Version".startswith(argv[0]): |
---|
809 | print("iproute2mac, v" + VERSION) |
---|
810 | exit(0) |
---|
811 | elif "-help".startswith(argv[0]): |
---|
812 | return False |
---|
813 | else: |
---|
814 | perror('Option "{}" is unknown, try "ip help".'.format(argv[0])) |
---|
815 | exit(255) |
---|
816 | |
---|
817 | if not argv: |
---|
818 | return False |
---|
819 | |
---|
820 | for cmd, cmd_func in cmds: |
---|
821 | if cmd.startswith(argv[0]): |
---|
822 | argv.pop(0) |
---|
823 | # Functions return true or terminate with exit(255) |
---|
824 | # See help_msg and do_help* |
---|
825 | return cmd_func(argv, af, json_print, pretty_json) |
---|
826 | |
---|
827 | perror('Object "{}" is unknown, try "ip help".'.format(argv[0])) |
---|
828 | exit(1) |
---|
829 | |
---|
830 | |
---|
831 | if __name__ == "__main__": |
---|
832 | main(sys.argv[1:]) |
---|