[64dea9f] | 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: |
---|
[dc7c6af] | 179 | (local, prefixlen) = re.findall(r"inet6 ((?:[a-f0-9:]+:+)+[a-f0-9]+)%*\w* +prefixlen (\d+)", r)[0] |
---|
[64dea9f] | 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:]) |
---|