package main

import (
	"errors"
	"net"
	"sync"
	"time"

	"github.com/jedisct1/dlog"
	"github.com/miekg/dns"
)

const rfc7050WKN = "ipv4only.arpa."

var (
	rfc7050WKA1 = net.IPv4(192, 0, 0, 170)
	rfc7050WKA2 = net.IPv4(192, 0, 0, 171)
)

type PluginDNS64 struct {
	pref64Mutex    *sync.RWMutex
	pref64         []*net.IPNet
	dns64Resolvers []string
	ipv4Resolver   string
	proxy          *Proxy
}

func (plugin *PluginDNS64) Name() string {
	return "dns64"
}

func (plugin *PluginDNS64) Description() string {
	return "Synthesize DNS64 AAAA responses"
}

func (plugin *PluginDNS64) Init(proxy *Proxy) error {
	if len(proxy.listenAddresses) == 0 {
		return errors.New("At least one listening IP address must be configured for the DNS64 plugin to work")
	}
	plugin.ipv4Resolver = proxy.listenAddresses[0] // query is sent to ourselves
	plugin.pref64Mutex = new(sync.RWMutex)
	plugin.proxy = proxy

	if len(proxy.dns64Prefixes) != 0 {
		plugin.pref64Mutex.Lock()
		defer plugin.pref64Mutex.Unlock()
		for _, prefStr := range proxy.dns64Prefixes {
			_, pref, err := net.ParseCIDR(prefStr)
			if err != nil {
				return err
			}
			dlog.Noticef("Registered DNS64 prefix [%s]", pref.String())
			plugin.pref64 = append(plugin.pref64, pref)
		}
	} else if len(proxy.dns64Resolvers) != 0 {
		plugin.dns64Resolvers = proxy.dns64Resolvers
		if err := plugin.refreshPref64(); err != nil {
			return err
		}
	} else {
		return nil
	}
	dlog.Notice("DNS64 map enabled")

	return nil
}

func (plugin *PluginDNS64) Drop() error {
	return nil
}

func (plugin *PluginDNS64) Reload() error {
	return nil
}

func (plugin *PluginDNS64) Eval(pluginsState *PluginsState, msg *dns.Msg) error {
	if hasAAAAAnswer(msg) {
		return nil
	}

	question := pluginsState.questionMsg.Question[0]
	if question.Qclass != dns.ClassINET || question.Qtype != dns.TypeAAAA {
		return nil
	}

	msgA := pluginsState.questionMsg.Copy()
	msgA.SetQuestion(question.Name, dns.TypeA)
	msgAPacket, err := msgA.Pack()
	if err != nil {
		return err
	}

	if !plugin.proxy.clientsCountInc() {
		return errors.New("Too many concurrent connections to handle DNS64 subqueries")
	}
	respPacket := plugin.proxy.processIncomingQuery(
		"trampoline",
		plugin.proxy.mainProto,
		msgAPacket,
		nil,
		nil,
		time.Now(),
		false,
	)
	plugin.proxy.clientsCountDec()
	resp := dns.Msg{}
	if err := resp.Unpack(respPacket); err != nil {
		return err
	}

	if resp.Rcode != dns.RcodeSuccess {
		return nil
	}

	if len(resp.Answer) == 0 {
		return nil
	}

	initialTTL := uint32(600)
	for _, ns := range resp.Ns {
		header := ns.Header()
		if header.Rrtype == dns.TypeSOA {
			initialTTL = header.Ttl
		}
	}

	synth64 := make([]dns.RR, 0)
	for _, answer := range resp.Answer {
		header := answer.Header()
		if header.Rrtype == dns.TypeCNAME {
			synth64 = append(synth64, answer)
		} else if header.Rrtype == dns.TypeA {
			ttl := initialTTL
			if ttl > header.Ttl {
				ttl = header.Ttl
			}

			ipv4 := answer.(*dns.A).A.To4()
			if ipv4 != nil {
				plugin.pref64Mutex.RLock()
				for _, prefix := range plugin.pref64 {
					ipv6 := translateToIPv6(ipv4, prefix)
					synthAAAA := new(dns.AAAA)
					synthAAAA.Hdr = dns.RR_Header{
						Name:   header.Name,
						Rrtype: dns.TypeAAAA,
						Class:  header.Class,
						Ttl:    ttl,
					}
					synthAAAA.AAAA = ipv6
					synth64 = append(synth64, synthAAAA)
				}
				plugin.pref64Mutex.RUnlock()
			}
		}
	}

	msg.Answer = synth64
	msg.AuthenticatedData = false
	msg.SetEdns0(uint16(MaxDNSUDPSafePacketSize), false)

	pluginsState.returnCode = PluginsReturnCodeCloak

	return nil
}

func hasAAAAAnswer(msg *dns.Msg) bool {
	for _, answer := range msg.Answer {
		if answer.Header().Rrtype == dns.TypeAAAA {
			return true
		}
	}
	return false
}

func translateToIPv6(ipv4 net.IP, prefix *net.IPNet) net.IP {
	ipv6 := make(net.IP, net.IPv6len)
	copy(ipv6, prefix.IP)
	n, _ := prefix.Mask.Size()
	ipShift := n / 8
	for i := 0; i < net.IPv4len; i++ {
		if ipShift+i == 8 {
			ipShift++
		}
		ipv6[ipShift+i] = ipv4[i]
	}
	return ipv6
}

func (plugin *PluginDNS64) fetchPref64(resolver string) error {
	msg := new(dns.Msg)
	msg.SetQuestion(rfc7050WKN, dns.TypeAAAA)

	client := new(dns.Client)
	resp, _, err := client.Exchange(msg, resolver)
	if err != nil {
		return err
	}

	if resp == nil || resp.Rcode != dns.RcodeSuccess {
		return errors.New("Unable to fetch Pref64")
	}

	uniqPrefixes := make(map[string]struct{})
	prefixes := make([]*net.IPNet, 0)
	for _, answer := range resp.Answer {
		if answer.Header().Rrtype == dns.TypeAAAA {
			ipv6 := answer.(*dns.AAAA).AAAA
			if ipv6 != nil && len(ipv6) == net.IPv6len {
				prefEnd := 0

				if wka := net.IPv4(ipv6[12], ipv6[13], ipv6[14], ipv6[15]); wka.Equal(rfc7050WKA1) ||
					wka.Equal(rfc7050WKA2) { // 96
					prefEnd = 12
				} else if wka := net.IPv4(ipv6[9], ipv6[10], ipv6[11], ipv6[12]); wka.Equal(rfc7050WKA1) || wka.Equal(rfc7050WKA2) { // 64
					prefEnd = 8
				} else if wka := net.IPv4(ipv6[7], ipv6[9], ipv6[10], ipv6[11]); wka.Equal(rfc7050WKA1) || wka.Equal(rfc7050WKA2) { // 56
					prefEnd = 7
				} else if wka := net.IPv4(ipv6[6], ipv6[7], ipv6[9], ipv6[10]); wka.Equal(rfc7050WKA1) || wka.Equal(rfc7050WKA2) { // 48
					prefEnd = 6
				} else if wka := net.IPv4(ipv6[5], ipv6[6], ipv6[7], ipv6[9]); wka.Equal(rfc7050WKA1) || wka.Equal(rfc7050WKA2) { // 40
					prefEnd = 5
				} else if wka := net.IPv4(ipv6[4], ipv6[5], ipv6[6], ipv6[7]); wka.Equal(rfc7050WKA1) || wka.Equal(rfc7050WKA2) { // 32
					prefEnd = 4
				}

				if prefEnd > 0 {
					prefix := new(net.IPNet)
					prefix.IP = append(ipv6[:prefEnd], net.IPv6zero[prefEnd:]...)
					prefix.Mask = net.CIDRMask(prefEnd*8, 128)
					if _, ok := uniqPrefixes[prefix.String()]; !ok {
						prefixes = append(prefixes, prefix)
						uniqPrefixes[prefix.String()] = struct{}{}
						dlog.Infof("Registered DNS64 prefix [%s]", prefix.String())
					}
				}
			}
		}
	}

	if len(prefixes) == 0 {
		return errors.New("Empty Pref64 list")
	}

	plugin.pref64Mutex.Lock()
	defer plugin.pref64Mutex.Unlock()
	plugin.pref64 = prefixes
	return nil
}

func (plugin *PluginDNS64) refreshPref64() error {
	for _, resolver := range plugin.dns64Resolvers {
		if err := plugin.fetchPref64(resolver); err == nil {
			break
		}
	}

	plugin.pref64Mutex.RLock()
	defer plugin.pref64Mutex.RUnlock()
	if len(plugin.pref64) == 0 {
		return errors.New("Empty Pref64 list")
	}

	return nil
}
