#!/usr/bin/env dnsjit
local clock = require("dnsjit.lib.clock")
local log = require("dnsjit.core.log")
local getopt = require("dnsjit.lib.getopt").new({
    { "v", "verbose", 0, "Enable and increase verbosity for each time given", "?+" },
    { "r", "read-state", "", "File to read state from before processing", "?" },
    { "w", "write-state", "", "File to write state to after processing", "?" },
})
local pcap = unpack(getopt:parse())
if getopt:val("help") then
    getopt:usage()
    return
end
local v = getopt:val("v")
if v > 0 then
    log.enable("warning")
end
if v > 1 then
    log.enable("notice")
end
if v > 2 then
    log.enable("info")
end
if v > 3 then
    log.enable("debug")
end

if pcap == nil then
    print("usage: "..arg[1].." <pcap>")
    return
end

local object = require("dnsjit.core.objects")
local input = require("dnsjit.input.mmpcap").new()
local layer = require("dnsjit.filter.layer").new()
local dns = require("dnsjit.core.object.dns").new()
local label = require("dnsjit.core.object.dns.label")
local ffi = require("ffi")
local labels = require("dnsjit.core.object.dns.label").new(16)
local q = require("dnsjit.core.object.dns.q").new()
local bit = require("bit")

hash_u32 = ffi.new("uint32_t[2]")
hash_u32p = ffi.cast("uint32_t*", hash_u32)
function hashkey(dns, transport, protocol)
    if transport.obj_type == object.IP then
        ffi.copy(hash_u32p, transport.src, 4)
        hash_u32[1] = hash_u32[0]
        ffi.copy(hash_u32p, transport.dst, 4)
        hash_u32[1] = bit.bxor(hash_u32[0], hash_u32[1])
    else
        local srcp = ffi.cast("uint8_t*", transport.src)
        ffi.copy(hash_u32p, srcp, 4)
        hash_u32[1] = hash_u32[0]
        ffi.copy(hash_u32p, srcp+4, 4)
        hash_u32[1] = bit.bxor(hash_u32[0], hash_u32[1])
        srcp = ffi.cast("uint8_t*", transport.dst)
        ffi.copy(hash_u32p, srcp, 4)
        hash_u32[1] = bit.bxor(hash_u32[0], hash_u32[1])
        ffi.copy(hash_u32p, srcp+4, 4)
        hash_u32[1] = bit.bxor(hash_u32[0], hash_u32[1])
    end
    if dns.qr == 1 then
        hash_u32[0] = protocol.dport + bit.lshift(protocol.sport, 16)
    else
        hash_u32[0] = protocol.sport + bit.lshift(protocol.dport, 16)
    end
    hash_u32[1] = bit.bxor(hash_u32[0], hash_u32[1])
    hash_u32[0] = dns.id + bit.lshift(dns.id, 16)
    return bit.bxor(hash_u32[0], hash_u32[1])
end

function dump_inflight(hkey, obj)
    return string.format("return %d, { qsec = %d, qnsec = %d, rsec = %d, rnsec = %d, src = %q, sport = %d, dst = %q, dport = %d, id = %d, qname = %q, qtype = %q, rcode = %d }",
        hkey,
        tonumber(obj.qsec), tonumber(obj.qnsec),
        tonumber(obj.rsec), tonumber(obj.rnsec),
        obj.src, obj.sport,
        obj.dst, obj.dport,
        obj.id,
        obj.qname, obj.qtype,
        obj.rcode
    )
end

function qrout(res)
    local tsq = tonumber(res.qsec) + (tonumber(res.qnsec)/1000000000)
    local tsr = tonumber(res.rsec) + (tonumber(res.rnsec)/1000000000)
    print(tsq, tsr, math.floor(((tsr-tsq)*1000000)+0.5),
        res.src, res.dst, res.id, res.rcode, res.qname, res.qtype)
end

input:open(pcap)
layer:producer(input)
local producer, ctx = layer:produce()

local inflight = {}

if getopt:val("read-state") > "" then
    local f, _ = io.open(getopt:val("read-state"))
    local inflights = 0
    if f ~= nil then
        for chunk in f:lines() do
            local hkey, query = assert(loadstring(chunk))()
            if hkey and query then
                if not inflight[hkey] then
                    inflight[hkey] = {
                        queries = {},
                        size = 0,
                    }
                end

                table.insert(inflight[hkey].queries, query)
                inflight[hkey].size = inflight[hkey].size + 1
                inflights = inflights + 1
            end
        end
        f:close()
        print(string.format("== read %d inflight states from %q", inflights, getopt:val("read-state")))
    end
end

local stat = {
    packets = 0,
    queries = 0,
    responses = 0,
    dropped = 0,
}
local start_sec, start_nsec = clock:monotonic()
while true do
    local obj = producer(ctx)
    if obj == nil then break end
    stat.packets = stat.packets + 1
    local pl = obj:cast()
    if obj:type() == "payload" and pl.len > 0 then
        local protocol = obj.obj_prev
        while protocol ~= nil do
            if protocol.obj_type == object.UDP or protocol.obj_type == object.TCP then
                break
            end
            protocol = protocol.obj_prev
        end
        local transport = protocol.obj_prev
        while transport ~= nil do
            if transport.obj_type == object.IP or transport.obj_type == object.IP6 then
                break
            end
            transport = transport.obj_prev
        end
        local pcap = transport.obj_prev
        while pcap ~= nil do
            if pcap.obj_type == object.PCAP then
                break
            end
            pcap = pcap.obj_prev
        end

        dns:reset()
        if protocol ~= nil and protocol.obj_type == object.TCP then
            dns.includes_dnslen = 1
        end
        dns.obj_prev = obj
        if pcap ~= nil and transport ~= nil and protocol ~= nil and dns:parse_header() == 0 then
            transport = transport:cast()
            protocol = protocol:cast()
            pcap = pcap:cast()

            local hkey = hashkey(dns, transport, protocol)

            if dns.qr == 1 then
                stat.responses = stat.responses + 1
                if inflight[hkey] then
                    for k, n in pairs(inflight[hkey].queries) do
                        if n.id == dns.id
                            and n.sport == protocol.dport
                            and n.dport == protocol.sport
                            and n.src == transport:destination()
                            and n.dst == transport:source()
                        then
                            n.rsec = pcap.ts.sec
                            n.rnsec = pcap.ts.nsec
                            n.rcode = dns.rcode
                            qrout(n)
                            inflight[hkey].queries[k] = nil
                            inflight[hkey].size = inflight[hkey].size - 1
                            if inflight[hkey].size < 1 then
                                inflight[hkey] = nil
                            end
                            break
                        end
                    end
                else
                    print("== dropped",
                        tonumber(pcap.ts.sec) + (tonumber(pcap.ts.nsec) / 1000000000),
                        transport:source(),
                        transport:destination(),
                        dns.id,
                        label.tooffstr(dns, labels, 16),
                        dns.type_tostring(q.type)
                    )
                    stat.dropped = stat.dropped + 1
                end
            else
                stat.queries = stat.queries + 1
                if dns.qdcount > 0 and dns:parse_q(q, labels, 16) == 0 then
                    if not inflight[hkey] then
                        inflight[hkey] = {
                            queries = {},
                            size = 0,
                        }
                    end

                    table.insert(inflight[hkey].queries, {
                        qsec = pcap.ts.sec,
                        qnsec = pcap.ts.nsec,
                        rsec = -1,
                        rnsec = -1,
                        src = transport:source(),
                        sport = protocol.sport,
                        dst = transport:destination(),
                        dport = protocol.dport,
                        id = dns.id,
                        qname = label.tooffstr(dns, labels, 16),
                        qtype = dns.type_tostring(q.type),
                        rcode = -1,
                    })
                    inflight[hkey].size = inflight[hkey].size + 1
                end
            end
        end
    end
end
local end_sec, end_nsec = clock:monotonic()

local runtime = 0
if end_sec > start_sec then
    runtime = ((end_sec - start_sec) - 1) + ((1000000000 - start_nsec + end_nsec)/1000000000)
elseif end_sec == start_sec and end_nsec > start_nsec then
    runtime = (end_nsec - start_nsec) / 1000000000
end

print("== runtime", runtime)
print("== packets", stat.packets, stat.packets/runtime)
print("== queries", stat.queries, stat.queries/runtime)
print("== responses", stat.responses, stat.responses/runtime)
print("== dropped", stat.dropped, stat.dropped/runtime)

if getopt:val("write-state") > "" then
    local f, _ = io.open(getopt:val("write-state"), "w+")
    local inflights = 0
    if f ~= nil then
        for hkey, unanswered in pairs(inflight) do
            for _, query in pairs(unanswered.queries) do
                f:write(dump_inflight(hkey, query), "\n")
                inflights = inflights + 1
            end
        end
        f:close()
        print(string.format("== wrote %d inflight states to %q", inflights, getopt:val("write-state")))
    end
else
    inflights = 0
    for hkey, unanswered in pairs(inflight) do
        inflights = inflights + unanswered.size
    end
    if inflights > 0 then
        print("== inflight queries (tsq, src, dst, id, qname, qtype)")
        for hkey, unanswered in pairs(inflight) do
            for _, query in pairs(unanswered.queries) do
                print(tonumber(query.qsec) + (tonumber(query.qnsec)/1000000000), query.src, query.dst, query.id, query.qname, query.qtype)
            end
        end
    end
end
