Files
2026-07-13 12:32:21 +08:00

333 lines
7.6 KiB
Lua

local utils = require("kong.resty.dns.utils")
local log = ngx.log
local NOTICE = ngx.NOTICE
local type = type
local ipairs = ipairs
local tonumber = tonumber
local math_random = math.random
local table_new = require("table.new")
local table_clear = require("table.clear")
local table_insert = table.insert
local table_remove = table.remove
local readlines = require("pl.utils").readlines
local DEFAULT_HOSTS_FILE = "/etc/hosts"
local DEFAULT_RESOLV_CONF = "/etc/resolv.conf"
local LOCALHOST = {
ipv4 = "127.0.0.1",
ipv6 = "[::1]",
}
local DEFAULT_HOSTS = { localhost = LOCALHOST, }
-- checks the hostname type
-- @return "ipv4", "ipv6", or "domain"
local function hostname_type(name)
local remainder, colons = name:gsub(":", "")
if colons > 1 then
return "ipv6"
end
if remainder:match("^[%d%.]+$") then
return "ipv4"
end
return "domain"
end
-- parses a hostname with an optional port
-- IPv6 addresses are always returned in square brackets
-- @param name the string to check (this may contain a port number)
-- @return `name/ip` + `port (or nil)` + `type ("ipv4", "ipv6" or "domain")`
local function parse_hostname(name)
local t = hostname_type(name)
if t == "ipv4" or t == "domain" then
local ip, port = name:match("^([^:]+)%:*(%d*)$")
return ip, tonumber(port), t
end
-- ipv6
if name:match("%[") then -- brackets, so possibly a port
local ip, port = name:match("^%[([^%]]+)%]*%:*(%d*)$")
return "[" .. ip .. "]", tonumber(port), t
end
return "[" .. name .. "]", nil, t -- no brackets also means no port
end
local function get_lines(path)
if type(path) == "table" then
return path
end
return readlines(path)
end
local function parse_hosts(path, enable_ipv6)
local lines, err = get_lines(path or DEFAULT_HOSTS_FILE)
if not lines then
log(NOTICE, "Invalid hosts file: ", err)
return DEFAULT_HOSTS
end
local hosts = {}
for _, line in ipairs(lines) do
-- Remove leading/trailing whitespaces and split by whitespace
local parts = {}
local n = 0
for part in line:gmatch("%S+") do
if part:sub(1, 1) == '#' then
break
end
n = n + 1
parts[n] = part:lower()
end
-- Check if the line contains an IP address followed by hostnames
if n >= 2 then
local ip, _, family = parse_hostname(parts[1])
if family ~= "domain" then -- ipv4/ipv6
for i = 2, n do
local host = parts[i]
local v = hosts[host]
if not v then
v = {}
hosts[host] = v
end
v[family] = v[family] or ip -- prefer to use the first ip
end
end
end
end
if not hosts.localhost then
hosts.localhost = LOCALHOST
end
return hosts
end
-- TODO: need to rewrite it instead of calling parseResolvConf from the old library
local function parse_resolv_conf(path, enable_ipv6)
local resolv, err = utils.parseResolvConf(path or DEFAULT_RESOLV_CONF)
if not resolv then
return nil, err
end
resolv = utils.applyEnv(resolv)
resolv.options = resolv.options or {}
resolv.ndots = resolv.options.ndots or 1
resolv.search = resolv.search or (resolv.domain and { resolv.domain })
-- check if timeout is 0s
if resolv.options.timeout then
if resolv.options.timeout <= 0 then
log(NOTICE, "A non-positive timeout of ", resolv.options.timeout,
"s is configured in resolv.conf. Setting it to 2000ms.")
resolv.options.timeout = 2000 -- 2000ms is lua-resty-dns default
else
-- convert resolv.conf timeout from seconds to milliseconds
resolv.options.timeout = resolv.options.timeout * 1000
end
end
-- remove special domain like "."
if resolv.search then
for i = #resolv.search, 1, -1 do
if resolv.search[i] == "." then
table_remove(resolv.search, i)
end
end
end
-- nameservers
if resolv.nameserver then
local n = 0
local nameservers = {}
for _, address in ipairs(resolv.nameserver) do
local ip, port, t = utils.parseHostname(address)
if t == "ipv4" or
(t == "ipv6" and not ip:find([[%]], nil, true) and enable_ipv6)
then
n = n + 1
nameservers[n] = port and { ip, port } or ip
end
end
resolv.nameservers = nameservers
end
return resolv
end
local function is_fqdn(name, ndots)
if name:sub(-1) == "." then
return true
end
local _, dot_count = name:gsub("%.", "")
return (dot_count >= ndots)
end
-- check if it matchs the SRV pattern: _<service>._<proto>.<name>
local function is_srv(name)
return name:match("^_[^._]+%._[^._]+%.[^.]+") ~= nil
end
-- construct names from resolv options: search, ndots and domain
local function search_names(name, resolv, hosts)
local resolv_search = resolv.search
if not resolv_search or is_fqdn(name, resolv.ndots) or
(hosts and hosts[name])
then
return { name }
end
local count = #resolv_search
local names = table_new(count + 1, 0)
for i = 1, count do
names[i] = name .. "." .. resolv_search[i]
end
names[count + 1] = name -- append the original name at last
return names
end
-- add square brackets around IPv6 addresses if a non-strict check detects them
local function ipv6_bracket(name)
if name:match("^[^[].*:") then -- not start with '[' and contains ':'
return "[" .. name .. "]"
end
return name
end
-- util APIs to balance @answers
local function get_next_round_robin_answer(answers)
answers.last = (answers.last or 0) % #answers + 1
return answers[answers.last]
end
local get_next_weighted_round_robin_answer
do
-- based on the Nginx's SWRR algorithm and lua-resty-balancer
local function swrr_next(answers)
local total = 0
local best = nil -- best answer in answers[]
for _, answer in ipairs(answers) do
-- 0.1 gives weight 0 record a minimal chance of being chosen (rfc 2782)
local w = (answer.weight == 0) and 0.1 or answer.weight
local cw = answer.cw + w
answer.cw = cw
if not best or cw > best.cw then
best = answer
end
total = total + w
end
best.cw = best.cw - total
return best
end
local function swrr_init(answers)
for _, answer in ipairs(answers) do
answer.cw = 0 -- current weight
end
-- random start
for _ = 1, math_random(#answers) do
swrr_next(answers)
end
end
-- gather records with the lowest priority in SRV record
local function filter_lowest_priority_answers(answers)
-- SRV record MUST have `priority` field
local lowest_priority = answers[1].priority
local l = {} -- lowest priority records list
for _, answer in ipairs(answers) do
if answer.priority < lowest_priority then
lowest_priority = answer.priority
table_clear(l)
l[1] = answer
elseif answer.priority == lowest_priority then
table_insert(l, answer)
end
end
answers.lowest_prio_records = l
return l
end
get_next_weighted_round_robin_answer = function(answers)
local l = answers.lowest_prio_records or filter_lowest_priority_answers(answers)
-- perform round robin selection on lowest priority answers @l
if not l[1].cw then
swrr_init(l)
end
return swrr_next(l)
end
end
return {
hostname_type = hostname_type,
parse_hostname = parse_hostname,
parse_hosts = parse_hosts,
parse_resolv_conf = parse_resolv_conf,
is_fqdn = is_fqdn,
is_srv = is_srv,
search_names = search_names,
ipv6_bracket = ipv6_bracket,
get_next_round_robin_answer = get_next_round_robin_answer,
get_next_weighted_round_robin_answer = get_next_weighted_round_robin_answer,
}