#!/usr/bin/lua

-- Created by jiaqiang.ye@lnxall.com
-- 2023/12/08
-- Simple /etc/resolv.conf monitor

local uci = require "uci"
local bit32 = require "bit32"
local invoker = require "invoker"

local gfmt = string.format
local g_dns = { [1] = "8.8.8.8", [2] = "114.114.114.114" }
local g_outtime = 3
local g_renum = 3

local function check_resolv()
	local has_sr = ""
	if invoker.statfile('/lib/systemd/systemd-resolved') then
		local conts, restart = {}, false
		local syscfg = '/etc/systemd/resolved.conf'

		conts[#conts + 1] = "# Generated by DNSWATCHER\n[Resolve]\n"
		conts[#conts + 1] = gfmt("DNS=%s %s\n", g_dns[1], g_dns[2])
		conts[#conts + 1] = "DNSSEC=no\n"
		conts[#conts + 1] = "Cache=yes\n"
		conts[#conts + 1] = "LLMNR=false\n"
		conts = table.concat(conts)

		local sdat = invoker.readfile(syscfg)
		if sdat ~= conts then
			restart = true
			local sfile = io.open(syscfg, "wb")
			if sfile then
				sfile:write(conts)
				sfile:close(); sfile = nil
			end
		end

		if not restart then
			local _, output = invoker.invoke(invoker.OUTPUT, "systemctl", "status", "systemd-resolved")
			if type(output) ~= "string" then output = "" end
			if not string.find(output, "Active: active (running)", 1, true) then
				restart = true
			end
		end

		if restart then
			io.stdout:write("Restarting systemd-resolved service...\n")
			io.stdout:flush()
			invoker.invoke(0, "systemctl", "enable", "systemd-resolved")
			invoker.invoke(0, "systemctl", "restart", "systemd-resolved")
		end
		has_sr = "nameserver 127.0.0.53\n"
	end

	local content = gfmt("# Generated by DNSWATCHER\n%snameserver %s\nnameserver %s\noptions timeout:%s attempts:%s\n",
		has_sr, g_dns[1], g_dns[2], g_outtime, g_renum)
	local rfile = "/etc/resolv.conf"
	local cont = invoker.readfile(rfile)
	if cont == content then return true end
	io.stdout:write(gfmt("Warning: Updating DNS settings: %s ...\n", rfile))
	io.stdout:flush()
	local resolv = io.open(rfile, "wb")
	if resolv then
		resolv:write(content)
		resolv:close(); resolv = nil
		invoker.sync()
		return true
	end
	return false
end

local function watch_resolv(delay)
	local flags = bit32.bor(invoker.IN_CREATE, invoker.IN_DELETE_SELF,
		invoker.IN_MODIFY, invoker.IN_CLOSE_WRITE)
	local rfile = '/etc/resolv.conf'
	local rpath = invoker.realpath(rfile)
	if type(rpath) == "string" then rfile = rpath end
	rpath = nil

	local changed, errmsg = invoker.watchfile(flags, delay * 1000, nil, rfile)
	if changed == nil then
		if type(errmsg) ~= "string" then errmsg = "unknown watchfile error" end
		io.stderr:write(errmsg); io.stderr:write("\n"); io.stderr:flush()
		invoker.waitsec(delay)
		return false
	end
	if changed and changed > 0 then
		invoker.msleep(1500) -- delay 1.5 second to avoid modifying by too many processes
		check_resolv()
	end
	return true
end

local function mainloop()
	invoker.setname("DNSWATCHER")
	local ucidns = uci.cursor('/etc/config')
	if ucidns then
		local dns1 = ucidns:get('dnsconfig', "@lnxdnscfg[0]", "dns1")
		local dns2 = ucidns:get('dnsconfig', "@lnxdnscfg[0]", "dns2")
		local outtime = ucidns:get('dnsconfig', "@lnxdnscfg[0]", "outtime")
		local renum = ucidns:get('dnsconfig', "@lnxdnscfg[0]", "attempts")
		ucidns:close(); ucidns = nil
		if invoker.isipv4(dns1) then g_dns[1] = dns1 end
		if invoker.isipv4(dns2) then g_dns[2] = dns2 end
		if outtime then
			outtime = tonumber(outtime)
			if outtime and outtime > 1 and outtime < 13 then g_outtime = outtime end
		end
		if attempts then
			attempts = tonumber(attempts)
			if attempts and attempts > 0 and attempts < 6 then g_renum = attempts end
		end
	end

	io.stdout:write(gfmt("DNS servers: %s, %s timeout:%s attempts:%s\n", g_dns[1], g_dns[2], g_outtime, g_renum))
	io.stdout:flush()

	-- delay 45 seconds before continue for fresh boot:
	local now = invoker.uptime()
	if now < 45 then
		invoker.waitsec(45 - now)
		now = invoker.uptime()
	end

	local nex = now + 900
	while true do
		check_resolv()
		now = invoker.uptime()
		while now < nex do
			watch_resolv(nex - now)
			now = invoker.uptime()
		end
		nex = nex + 900
	end
end
mainloop()
os.exit(1)
