OK, so this thing has two parts:

  1. when to trigger the fallback
  2. switching to forwarding mode itself

Yes, this is all packets being dropped to their nameserver IP addresses, so no response at all on either UDP or TCP.
1: for the trigger I'd suggest reusing the logic from our serve_stale module, as that one is (also) meant as fallback in case upstream servers don't reply, and the reason for not replying doesn't seem to matter here.

2: the current server-selection code does not count on being switched mid-request, but I hacked around that and I suspect it will work reasonably reliably... at least until we make some significant server-selection code change in farther future.  (note: the way I wrote it requires knot-resolver >= 5.3.0)

Overall, this seemed OK on my quick test:

-- add to normal config, typically in /etc/knot-resolver/kresd.conf
modules.load('fallback')

and then a separate module file:

-- in /var/lib/knot-resolver/kres_modules/fallback.lua (lua load path is somewhat customizable, too)
local M = {}
-- Default settings; action: probably works just with FORWARD/TLS_FORWARD.
M.timeout = 1 * sec
M.action = policy.FORWARD({'1.1.1.1', '1.0.0.1'})

local ffi = require('ffi')
ffi.cdef("void kr_server_selection_init(struct kr_query *qry);")

M.layer = {}
function M.layer.produce(state, req, pkt)
	local qry = req:current()
	if qry.flags.FORWARD then return end -- we've switched already
	local now = ffi.C.kr_now()
	local deadline = qry.creation_time_mono + M.timeout
	if now > deadline or qry.flags.NO_NS_FOUND then
		if verbose() then
			log('[     ][fall]   => no reachable NS, using fallback action')
		end
		M.action(state, req)
		-- Hacky: we need to reset the server-selection state,
		-- so that forwarding mode can start.
		-- Fortunately context is on kr_request mempool, so we can leak it.
		ffi.C.kr_server_selection_init(qry);
	end
end

return M

--Vladimir