#!/usr/bin/env python3
import argparse
from ipaddress import *
from nullroute.core import Core
import subprocess
import sys

IPV6_MCAST_SCOPES = {
    0x0: "reserved",
    0x1: "interface-local",
    0x2: "link-local",
    0x4: "admin-local",
    0x5: "site-local",
    0x8: "organization-local",
    0xe: "global",
    0xf: "reserved",
}

IPV4_MCAST_BLOCKS = [
    ("224.0.0.0/24", "subnet-local", "Local Network Control Block (IANA)"),
    ("224.0.1.0/24", "global", "Internetwork Control Block (IANA)"),
    ("224.0.0.0/16", "global", "ad-hoc (IANA)"),
    ("224.1.0.0/16", "reserved", "IANA (RFC 5771)"),
    ("224.2.0.0/16", "?", "SDP/SAP (RFC 5771)"),
    ("224.3.0.0/16", "global", "ad-hoc (IANA)"),
    ("224.4.0.0/16", "global", "ad-hoc (IANA)"),
    ("232.0.0.0/8", "source-specific", None),
    ("233.252.0.0/14", "global", "ad-hoc (IANA)"),
    ("233.0.0.0/8", "global", "AS-based/GLOP (RFC 3180)"),
    ("234.0.0.0/8", "global", "unicast-prefix-based (RFC 6034)"),
    ("239.0.0.0/8", "organization", "RFC 1884/RFC 2365"),
    ("224.0.0.0/4", "reserved", "IANA (RFC 5771)"),
]

IPV4_MCAST_GROUPS = {
    # 224.0.0.0/24
    "224.0.0.1":        "All Nodes",
    "224.0.0.2":        "All Routers",
    "224.0.0.5":        "OSPF all routers",
    "224.0.0.6":        "OSPF designated routers",
    "224.0.0.9":        "RIP v2 routers",
    "224.0.0.13":       "PIM v2 routers",
    "224.0.0.19":       "IS-IS over IP",
    "224.0.0.20":       "IS-IS over IP",
    "224.0.0.21":       "IS-IS over IP",
    "224.0.0.22":       "IGMP v3",
    "224.0.0.102":      "HSRP v2 & GLBP load-balancing",
    "224.0.0.251":      "mDNS (Multicast DNS)",
    "224.0.0.252":      "LLMNR (Link-local multicast name resolution)",
    # 224.0.1.0/24
    "224.0.1.1":        "NTP (Network Time Protocol)",
    "224.0.1.3":        "rwhod",
    "224.0.1.20":       "(any private experiment)",
    "224.0.1.60":       "HP device discovery",
    # 226/8 reserved
    "226.178.217.5":    "possibly Spybot Search&Destroy",
    # 232.0.0.0/8
    # 233.0.0.0/8
    "233.89.188.1":     "Ubiquiti discovery",
    # 234.0.0.0/8
    # 239.0.0.0/8
    "239.192.0.0":      "BitTorrent Sync LPD",
    "239.192.152.143":  "BitTorrent LPD (Local Peer Discovery)",
    "239.255.255.250":  "SSDP (UPnP)",
}

IPV6_MCAST_GROUPS = {
    "ff02::1": "All Nodes",
    "ff02::2": "All Routers",
    "ff02::c": "SSDP",
    "ff02::d": "All PIM routers",
    "ff02::f": "UPnP",
    "ff02::16": "MLD v2",
    "ff02::fb": "mDNS (Multicast DNS)",
}

def nprint(k, *v):
    width = 12
    print("\033[92m%*s\033[m " % (width, k), *v)

def ipv4_is_netmask(addr):
    if addr.version != 4:
        return False
    want = 255
    mid = {
        0b11111110,
        0b11111100,
        0b11111000,
        0b11110000,
        0b11100000,
        0b11000000,
        0b10000000,
        0b00000000,
    }
    for x in addr.packed:
        if x in mid:
            want = 0
        elif x != want:
            return False
    return True

def ipv4_flip_netmask(addr):
    return ip_address(bytes([255-x for x in addr.packed]))

def ipv4_mcast_describe(addr):
    for mask, scope, desc in IPV4_MCAST_BLOCKS:
        if addr in ip_network(mask):
            return mask, scope, desc

def ipv6_is_isatap(addr):
    if addr.version != 6:
        return False
    return (addr.packed[8:12] in {b'\x00\x00\x5e\xfe', b'\x02\x00\x5e\xfe'})

def ipv6_maybe_v4mapped(addr):
    if addr.version != 6:
        return False
    return (addr.packed[8:12] == b'\x00\x00\x00\x00') \
            and (0 < addr.packed[12] < 224)

def ipv6_is_eui48(addr):
    if addr.version != 6:
        return False
    return (addr.packed[11:13] == b'\xff\xfe')

def ipv6_get_eui48(addr):
    mac = list(addr.packed[8:11] + addr.packed[13:])
    mac[0] ^= 2
    return mac

def ipv6_eui48_is_local(addr):
    if addr.version != 6:
        return False
    return not (addr.packed[8] & 0x2)

def eui48_fmt(addr):
    return ":".join(["%02x" % n for n in addr])

def eui48_get_vendor_nmap(addr):
    # Only 24-bit OUIs, not very up-to-date
    oui = "".join(["%02X" % x for x in addr[:3]])
    path = "/usr/share/nmap/nmap-mac-prefixes"
    try:
        with open(path, "r") as fh:
            for line in fh:
                line = line.strip().split(" ", 1)
                if line and line[0] == oui:
                    return line[1]
    except FileNotFoundError as e:
        Core.debug("nmap MAC database not available: %s", e)
    return None

def eui48_get_vendor_hwdb(addr):
    # External process, but frequently updated and has all OUI lengths
    query = "OUI:%s" % "".join(["%02X" % x for x in addr])
    try:
        with subprocess.Popen(["systemd-hwdb", "query", query],
                              stdin=subprocess.DEVNULL,
                              stdout=subprocess.PIPE) as proc:
            for line in proc.stdout:
                line = line.decode().strip().split("=", 1)
                if line[0] == "ID_OUI_FROM_DATABASE":
                    return line[1]
    except FileNotFoundError as e:
        Core.debug("systemd-hwdb not available: %s", e)
    return None

def eui48_get_whatmac(addr):
    query = eui48_fmt(addr)
    r = subprocess.run(["eui48_get_whatmac", "-q", query],
                       stdout=subprocess.PIPE)
    return r.stdout.decode().strip()

def ipv6_get_v4mapped(addr, at=12):
    if addr.version == 4:
        return addr
    return IPv4Address(addr.packed[at:at+4])

def addr2props(addr):
    kind = "unknown"
    misc = []
    if addr.version == 6:
        is_autoconf = False
        if addr.is_loopback:
            kind = "loopback"
        elif addr.is_unspecified:
            kind = "unspecified"
        elif addr.teredo:
            kind = "global (Teredo)"
            misc.append(("relay host", addr.teredo[0]))
            misc.append(("client v4", addr.teredo[1]))
        elif addr.sixtofour:
            kind = "global (6to4)"
            misc.append(("client v4", addr.sixtofour))
            is_autoconf = True
        elif addr.ipv4_mapped:
            kind = "IPv4-mapped"
            misc.append(("v4 address", addr.ipv4_mapped))
        elif addr in IPv6Network("::/96"):
            kind = "IPv4-compatible"
            misc.append(("v4 address", ipv6_get_v4mapped(addr)))
        elif addr in IPv6Network("64:ff9b::/96"):
            kind = "NAT64"
            misc.append(("v4 address", ipv6_get_v4mapped(addr)))
        elif addr.is_link_local:
            kind = "link-local"
            is_autoconf = True
        elif addr.is_site_local:
            kind = "site-local"
            is_autoconf = True
        elif addr.is_multicast:
            kind = "multicast"
            flags = (addr.packed[1] >> 4) & 0xF
            scope = addr.packed[1] & 0xF
            scope = IPV6_MCAST_SCOPES.get(scope, "invalid scope")
            misc.append(("scope", scope))
            group_name = IPV6_MCAST_GROUPS.get(str(addr))
            if group_name:
                misc.append(("usage", group_name))
        elif addr in IPv6Network("fc00::/7"):
            kind = "unique local address"
        elif addr in IPv6Network("2001:10::/28"):
            kind = "global (or deprecated ORCHIDv1)"
        elif addr in IPv6Network("2001:20::/28"):
            kind = "ORCHIDv2"
        elif addr in IPv6Network("2001:db8::/32"):
            kind = "reserved (documentation)"
        elif addr.is_reserved:
            if addr in IPv6Network("100::/64"):
                kind = "reserved (discard)"
            else:
                kind = "reserved"
        else:
            kind = "global"
            is_autoconf = True
            # Rackray/IV
            if addr in IPv6Network("2a02:7b40::/32"):
                misc.append(("v4 address", ipv6_get_v4mapped(addr, at=4)))

        if is_autoconf:
            if ipv6_is_eui48(addr):
                mac = ipv6_get_eui48(addr)
                misc.append(("MAC address", eui48_fmt(mac)))
                if vnd := eui48_get_vendor_hwdb(mac):
                    misc.append(("MAC vendor", vnd))
                if desc := eui48_get_whatmac(mac):
                    misc.append(("eui48_get_whatmac", desc))
            elif ipv6_is_isatap(addr):
                misc.append(("ISATAP v4", ipv6_get_v4mapped(addr)))
            elif ipv6_maybe_v4mapped(addr):
                misc.append(("mapped v4", ipv6_get_v4mapped(addr)))
            else:
                misc.append(("MAC address", "not an EUI48 interface ID"))
            if ipv6_eui48_is_local(addr):
                misc.append(("admin bit", "locally administered"))
    elif addr.version == 4:
        if addr.is_loopback:
            kind = "loopback"
        elif addr.is_unspecified:
            kind = "unspecified"
        elif addr in ip_network("255.255.255.255/32"):
            kind = "broadcast"
        elif addr.is_multicast:
            kind = "multicast"

            group_name = IPV4_MCAST_GROUPS.get(str(addr))
            if group_name:
                misc.append(("usage", group_name))

            _ = ipv4_mcast_describe(addr)
            if _:
                range, scope, desc = _
                misc.append(("scope", scope))
                misc.append(("range", "%s - %s" % (range, desc)))

            if addr in ip_network("233.0.0.0/8"):
                as_num = (addr.packed[1] << 8) | addr.packed[2]
                misc.append(("seed", "AS %d" % as_num))
            elif addr in ip_network("234.0.0.0/8"):
                unicast = ip_address( (addr.packed[1] << 24) \
                                    | (addr.packed[2] << 16) \
                                    | (addr.packed[3] <<  8) )
                misc.append(("seed", unicast))
        elif addr.is_link_local:
            kind = "link-local"
        elif addr.is_reserved:
            if addr in ip_network("240.0.0.0/4"):
                kind = "reserved (class-E)"
            else:
                kind = "reserved (unknown type)"
        elif addr in ip_network("100.64.0.0/10"):
            kind = "shared (RFC 6598)"
        elif addr.is_private:
            if   addr in ip_network("10.0.0.0/8") or \
                 addr in ip_network("172.16.0.0/12") or \
                 addr in ip_network("192.168.0.0/16"):
                kind = "private (RFC 1918)"
            elif addr in ip_network("192.0.2.0/24") or \
                 addr in ip_network("198.51.100.0/24") or \
                 addr in ip_network("203.0.113.0/24"):
                kind = "reserved (documentation)"
            else:
                kind = "private (unknown type)"
        elif addr.is_global:
            kind = "global"
        else:
            kind = "unknown"

    return kind, misc

def show_addr(addr):
    nprint("address", addr)
    if addr.version == 6:
        nprint("->", addr.exploded)
    kind, misc = addr2props(addr)
    nprint("type", kind)
    for k, v in misc:
        nprint(k, v)

def show_net(net, *, cisco=False):
    nprint("network", net)
    if net.version == 4:
        nprint("netmask", net.netmask, "(%d addresses)" % net.num_addresses)
        if cisco:
            nprint("wildcard", ipv4_flip_netmask(net.netmask))
    if net.prefixlen < net.max_prefixlen:
        nprint("first addr", net.network_address.exploded)
        nprint("last addr", net.broadcast_address.exploded)

    try:
        next_net = net.broadcast_address + 1
    except:
        pass
    else:
        next_net = ip_network("%s/%s" % (next_net, net.prefixlen))
        nprint("next net", next_net)

parser = argparse.ArgumentParser()
parser.add_argument("--cisco", action="store_true",
                    help="display Cisco IOS wildcard masks")
parser.add_argument("arg", nargs="+")
args = parser.parse_args()

i = -1
for i, arg in enumerate(args.arg):
    if i > 0:
        print()
    try:
        if arg.startswith("/"):
            plen = int(arg[1:])
            if plen < 0:
                Core.die("prefix length must be positive")
            elif plen > 128:
                Core.die("prefix length too large for any family")

            nprint("prefix", "/%d" % plen)
            if plen <= 32:
                net = ip_network("0.0.0.0/%d" % plen)
                nprint("v4 netmask", net.netmask, "(%d addresses)" % net.num_addresses)
                if args.cisco:
                    nprint("wildcard", ipv4_flip_netmask(net.netmask))
            net = ip_network("::/%d" % plen)
            nprint("v6 netmask", net.netmask.exploded)
        elif "/" in arg:
            addr = ip_interface(arg)
            show_addr(addr)
            print()
            show_net(addr.network, cisco=args.cisco)
        else:
            addr = ip_address(arg)
            if addr.version == 4 and ipv4_is_netmask(addr):
                net = ip_network("0.0.0.0/%s" % arg)
                nprint("netmask", net.netmask)
                if args.cisco:
                    nprint("wildcard", ipv4_flip_netmask(net.netmask))
                nprint("prefix", "/%d" % net.prefixlen)
                nprint("size", net.num_addresses)
            else:
                show_addr(addr)
    except ValueError as e:
        Core.die(str(e))
if i < 0:
    Core.die("not enough arguments")
