3proxy/tests/cases/sysdns.py
Vladimir Dubrovin d5e78a0900 resolver: the system's name servers and hosts file by default
Without nserver (or fakeresolve) names were resolved by the C library.
3proxy's own resolver is now the default, with the system's name
servers: up to 3 from /etc/resolv.conf, on Windows from the registry
(set for the system, set for an interface with an address, Tcpip6
interfaces, then those DHCP gave). With none found, the C library as
before. nserver takes up to 3 as well (MAXNSERVERS), a fourth is
warned about.

The name servers in force belong to the process, not a configuration:
nserver lines are kept aside while a configuration is read and replace
them only once it is committed, so a reload resolves with those in
force until then. Resolutions and dnspr read a copy of them.

The system's are read at start, before the configuration and a chroot
in it, and are in force while it is read, so its host names resolve
with them; read again at a commit, and checked every 5 seconds from
the main loop while the default is in force. What cannot be read then
(within a chroot) stays as last read.

The hosts file, read with them, is imported into tables of its own
while the default is in force: its first address of a family for a
name. A name is looked up as one nsrecord made first, then the hosts
file, then the name cache, then name servers. A changed file replaces
what was imported. No name cache is made for it: without nscache
nothing is cached, as before.

The name cache's keys are host names of any case now: DNS names are.

Tests: sysdns, a name of the hosts file answered by dnspr without
nserver, in any case, and not with one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 18:49:21 +03:00

84 lines
2.4 KiB
Python

"""The system's name servers and hosts file as the default resolver: with
no nserver in a configuration, names in the hosts file are answered from
it, through the name cache; with nserver lines, the file is not used."""
import os
import re
import sys
import time
def hosts_path():
if sys.platform == "win32":
return os.path.join(os.environ.get("SystemRoot", r"C:\Windows"),
"System32", "drivers", "etc", "hosts")
return "/etc/hosts"
def hosts_localhost():
"""Whether the hosts file maps localhost to 127.0.0.1 itself."""
try:
with open(hosts_path(), errors="replace") as f:
for line in f:
words = line.split("#", 1)[0].split()
if len(words) > 1 and words[0] == "127.0.0.1" and "localhost" in [w.lower() for w in words[1:]]:
return True
except OSError:
pass
return False
def system_nameserver():
"""Whether there is a name server for 3proxy to find: resolv.conf has
one, or on Windows the registry is taken to."""
if sys.platform == "win32":
return True
try:
with open("/etc/resolv.conf") as f:
return any(re.match(r"nameserver\s+[0-9a-fA-F.:]+\s*$", l.split("#")[0]) for l in f)
except OSError:
return False
def wait(t, port, name):
deadline = time.time() + 5
while time.time() < deadline:
if t.dns_query(port, name):
return
def run(t):
if not hosts_localhost() or not system_nameserver():
t.skip("no localhost in the hosts file, or no system name server")
return
port = t.free_port()
t.start("sysdns", f"""
log
flush
auth iponly
allow *
dnspr -p{port}
""")
wait(t, port, "localhost")
t.eq(["127.0.0.1"], t.dns_query(port, "localhost"),
"without nserver, a name of the hosts file is answered from it")
t.eq(["127.0.0.1"], t.dns_query(port, "LocalHost"),
"whatever its case")
# with a name server configured, one that answers nothing, the hosts
# file is not used
other = t.free_port()
t.start("sysdns_nserver", f"""
log
flush
nserver 127.0.0.1:{t.free_port()}
nscache 1024
auth iponly
allow *
dnspr -p{other}
""")
wait(t, other, "localhost")
t.ne(["127.0.0.1"], t.dns_query(other, "localhost") or [],
"with nserver, the hosts file is not used")