From d5e78a090026ed49490e184581132e032e54d804 Mon Sep 17 00:00:00 2001
From: Vladimir Dubrovin <3proxy@3proxy.ru>
Date: Tue, 20 Oct 2026 18:49:21 +0300
Subject: [PATCH] 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
---
doc/html/man5/3proxy.cfg.5.html | 30 ++-
man/3proxy.cfg.5 | 22 +-
src/3proxy.c | 3 +
src/conf.c | 41 +++-
src/dnspr.c | 17 +-
src/hashtables.c | 20 +-
src/plugins.c | 4 +-
src/proxy.h | 10 +
src/resolve.c | 397 +++++++++++++++++++++++++++++++-
src/structures.h | 7 +-
tests/cases/sysdns.py | 83 +++++++
11 files changed, 589 insertions(+), 45 deletions(-)
create mode 100644 tests/cases/sysdns.py
diff --git a/doc/html/man5/3proxy.cfg.5.html b/doc/html/man5/3proxy.cfg.5.html
index 7cd5d7a..d424c64 100644
--- a/doc/html/man5/3proxy.cfg.5.html
+++ b/doc/html/man5/3proxy.cfg.5.html
@@ -500,14 +500,28 @@ experimental.
nserver
<ipaddr>[:port][/tcp]
-Nameserver to use for name resolutions. If none specified
-system routines for name resolution is used. Optional port
-number may be specified. If optional /tcp is added to IP
+Nameserver to use for name resolutions; up to 3, asked in
+order. Optional port number may be specified. If optional /tcp is added to IP
address, name resolution is performed over TCP. An IPv6 address
has to be enclosed in square brackets:
nserver 1.1.1.1
nserver [2001:4860:4860::8844]
-nserver [2001:4860:4860::8844]:5353/tcp
+nserver [2001:4860:4860::8844]:5353/tcp
+If none is specified (and there is no fakeresolve),
+the system's name servers are used: up to 3 from
+/etc/resolv.conf, on Windows from the registry (those
+set for the system, those set for an interface, then those
+DHCP gave). Names in the hosts file (/etc/hosts, on
+Windows %SystemRoot%\System32\drivers\etc\hosts)
+are answered from it, before them, and names in the
+configuration are resolved this way too. Both are read
+before the configuration, so before a chroot, and
+checked for changes every 5 seconds; what cannot be read
+later (within a chroot) stays as last read. If no
+name server is found, the system routines for name
+resolution are used. The nserver lines of a
+configuration take effect when it is loaded: during a
+reload the name servers in force are used.
authnserver
@@ -533,9 +547,11 @@ rather than per-service.
Adds static record to nscache. nscache must be
enabled and must come first, because the record is placed in
the table it allocates - nscache6 for a record naming
-an IPv6 address - and nserver must be set as well:
-without it the system resolver is used and static records
-are never consulted. If 0.0.0.0 is used as a hostaddr host
+an IPv6 address - and names must be resolved by name servers
+(nserver, or the system's ones by default): with the
+system routines for name resolution static records are never
+consulted. A static record takes precedence over the hosts
+file. If 0.0.0.0 is used as a hostaddr host
will never resolve, it can be used to blacklist something or
together with dialer command to set up UDL for
dialing.
diff --git a/man/3proxy.cfg.5 b/man/3proxy.cfg.5
index a3f0285..b03c629 100644
--- a/man/3proxy.cfg.5
+++ b/man/3proxy.cfg.5
@@ -543,9 +543,8 @@ Use \fBauthcache\fR to speedup authentication. RADIUS feature is currently exper
.BR nserver
\fI\fR[:\fIport\fR][/\fItcp\fR]
.br
- Nameserver to use for name resolutions. If none specified
-system routines for name resolution is
-used. Optional port number may be specified.
+ Nameserver to use for name resolutions; up to 3, asked in order.
+Optional port number may be specified.
If optional /tcp is added to IP address, name resolution is
performed over TCP.
An IPv6 address has to be enclosed in square brackets:
@@ -555,6 +554,18 @@ An IPv6 address has to be enclosed in square brackets:
\fBnserver [2001:4860:4860::8844]\fR
.br
\fBnserver [2001:4860:4860::8844]:5353/tcp\fR
+.br
+ If none is specified (and there is no \fBfakeresolve\fR), the system's name
+servers are used: up to 3 from \fI/etc/resolv.conf\fR, on Windows from the
+registry (those set for the system, those set for an interface, then those
+DHCP gave). Names in the hosts file (\fI/etc/hosts\fR, on Windows
+\fI%SystemRoot%\\System32\\drivers\\etc\\hosts\fR) are answered from it,
+before them, and names in the configuration are resolved this way too. Both
+are read before the configuration, so before a \fBchroot\fR, and checked for
+changes every 5 seconds; what cannot be read later (within a \fBchroot\fR)
+stays as last read. If no name server is found, the system routines for name
+resolution are used. The \fBnserver\fR lines of a configuration take effect
+when it is loaded: during a reload the name servers in force are used.
.br
.BR authnserver
@@ -583,8 +594,9 @@ caches are global rather than per-service.
Adds static record to nscache. \fBnscache\fR must be enabled and must come
first, because the record is placed in the table it allocates - \fBnscache6\fR
for a record naming an IPv6 address - and
-\fBnserver\fR must be set as well: without it the system resolver is used and
-static records are never consulted. If 0.0.0.0
+names must be resolved by name servers (\fBnserver\fR, or the system's
+ones by default): with the system routines for name resolution static records
+are never consulted. A static record takes precedence over the hosts file. If 0.0.0.0
is used as a hostaddr host will never resolve, it can be used to
blacklist something.
diff --git a/src/3proxy.c b/src/3proxy.c
index 4559be2..7dfb90a 100644
--- a/src/3proxy.c
+++ b/src/3proxy.c
@@ -293,6 +293,7 @@ void cyclestep(void){
needreload = 0;
}
doschedule();
+ sysdnscheck();
if(conf->stdlog)fflush(conf->stdlog);
if(timechanged(minutecounter, curtime, MINUTELY)) {
struct filemon *fm;
@@ -555,6 +556,7 @@ int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int
#ifdef __linux__
nsopen(); /* before a chroot in the configuration takes /proc away */
#endif
+ sysdnsinit(); /* the system's name servers and hosts, before a chroot too */
_3proxy_mutex_init(&config_mutex);
_3proxy_mutex_init(&confref_mutex);
_3proxy_mutex_init(&listener_mutex);
@@ -592,6 +594,7 @@ int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int
/* numbered before it is read, as a reload numbers the new one */
conf->version++;
res = readconfig(fp);
+ if(!res) confdnscommit(conf);
resolvfunc = conf->resolvfunc;
/* readconfig() names the failing line, which an exit status would cut
diff --git a/src/conf.c b/src/conf.c
index 7b63dd1..7e08461 100644
--- a/src/conf.c
+++ b/src/conf.c
@@ -251,6 +251,8 @@ static int startservice(void *service){
return 0;
}
+static int noresolver(void);
+
static int h_proxy(int argc, unsigned char ** argv){
struct child ch;
@@ -267,7 +269,7 @@ static int h_proxy(int argc, unsigned char ** argv){
childdef.helpmessage = " -Xftp - fetch ftp:// URLs\n";
#endif
#ifdef NOIPV6
- if(!conf->resolvfunc || (conf->resolvfunc == myresolver && !dns_table.poolsize)){
+ if(noresolver()){
fprintf(stderr, "[line %d] Warning: no nserver/nscache configured, proxy may run very slow\n", linenum);
}
#endif
@@ -306,7 +308,7 @@ static int h_proxy(int argc, unsigned char ** argv){
childdef.isudp = 0;
childdef.service = S_SOCKS;
#ifdef NOIPV6
- if(!conf->resolvfunc || (conf->resolvfunc == myresolver && !dns_table.poolsize)){
+ if(noresolver()){
fprintf(stderr, "[line %d] Warning: no nserver/nscache configured, socks may run very slow\n", linenum);
}
#endif
@@ -374,7 +376,7 @@ static int h_proxy(int argc, unsigned char ** argv){
childdef.service = S_DNSPR;
childdef.helpmessage = " -s - simple DNS forwarding - do not use 3proxy resolver / name cache\n -Fip - fake: answer all A queries with this IP\n";
#ifndef NOIPV6
- if(!conf->resolvfunc || (conf->resolvfunc == myresolver && !dns_table.poolsize) || conf->resolvfunc == fakeresolver){
+ if(noresolver() || conf->resolvfunc == fakeresolver){
fprintf(stderr, "[line %d] Warning: no nserver/nscache configured, dnspr will not work as expected\n", linenum);
}
#endif
@@ -735,25 +737,43 @@ static int h_flushusers(int argc, unsigned char **argv){
}
*/
+/* The name servers of the configuration being read: they replace those in
+ force (the process has one list) once it is committed */
+static struct nserver cfgns[MAXNSERVERS];
+static int cfgnsn = 0;
+
static int h_nserver(int argc, unsigned char **argv){
char *str;
- if(conf->numservers < MAXNSERVERS) {
+ if(cfgnsn < MAXNSERVERS) {
+ memset(cfgns + cfgnsn, 0, sizeof(*cfgns));
if((str = strchr((char *)argv[1], '/')))
*str = 0;
- *SAPORT(&conf->nservers[conf->numservers].addr) = htons(53);
- if(parsehost(46, argv[1], (struct sockaddr *)&conf->nservers[conf->numservers].addr)) return 1;
+ *SAPORT(&cfgns[cfgnsn].addr) = htons(53);
+ if(parsehost(46, argv[1], (struct sockaddr *)&cfgns[cfgnsn].addr)) return 1;
if(str) {
- conf->nservers[conf->numservers].usetcp = strstr(str + 1, "tcp")? 1:0;
+ cfgns[cfgnsn].usetcp = strstr(str + 1, "tcp")? 1:0;
*str = '/';
}
- conf->numservers++;
-
+ cfgnsn++;
}
+ else fprintf(stderr, "[line %d] Warning: only %d name servers are used, nserver ignored\n", linenum, MAXNSERVERS);
conf->resolvfunc = myresolver;
return 0;
}
+/* The configuration's name servers, or the system's in their place */
+void confdnscommit(struct extparam *c){
+ sysdnsapply(c, cfgns, cfgnsn);
+}
+
+/* Whether names would be resolved slowly: by the C library, or by name
+ servers, the system's taken by default, without a cache */
+static int noresolver(void){
+ if(!conf->resolvfunc && !sysdnscount()) return 1;
+ return (!conf->resolvfunc || conf->resolvfunc == myresolver) && !dns_table.poolsize;
+}
+
static int h_authnserver(int argc, unsigned char **argv){
char *str;
@@ -2513,6 +2533,8 @@ void freeconf(struct extparam *confp){
void confbegin(void){
char * args[] = {"auth", "iponly", NULL};
+ cfgnsn = 0;
+
h_auth(2, (unsigned char **)args);
}
@@ -2643,6 +2665,7 @@ int reload (void){
_3proxy_mutex_lock(&confref_mutex);
runconf = new;
_3proxy_mutex_unlock(&confref_mutex);
+ confdnscommit(new);
resolvfunc = new->resolvfunc;
/* the new services take over the listening sockets of the old ones on
the same addresses, which then stop accepting, and start */
diff --git a/src/dnspr.c b/src/dnspr.c
index 3215192..82be693 100644
--- a/src/dnspr.c
+++ b/src/dnspr.c
@@ -146,19 +146,20 @@ static int dnsreply(struct udploop *u, struct udpsession *ss, unsigned char *buf
static int dnsforward(struct udploop *u, struct udpsession *ss){
struct clientparam *param = ss->param;
struct dnsquery *q = ss->data;
- struct extparam *c = param->srv->config;
+ struct nserver ns[MAXNSERVERS];
+ int n = getnservers(ns);
#ifdef _WIN32
unsigned long ul = 1;
#endif
- for(; q->server < c->numservers; q->server++){
+ for(; q->server < n; q->server++){
if(param->remsock != INVALID_SOCKET){
param->srv->so._closesocket(param->sostate, param->remsock);
param->remsock = INVALID_SOCKET;
}
/* another socket for the loop to watch */
ss->regsock = INVALID_SOCKET;
- q->tcp = c->nservers[q->server].usetcp;
+ q->tcp = ns[q->server].usetcp;
q->got = 0;
{
unsigned short id = (unsigned short)myrand();
@@ -166,7 +167,7 @@ static int dnsforward(struct udploop *u, struct udpsession *ss){
q->query[2] = (unsigned char)(id >> 8);
q->query[3] = (unsigned char)(id & 0xff);
}
- if((param->remsock = param->srv->so._socket(param->sostate, SASOCK(&c->nservers[q->server].addr),
+ if((param->remsock = param->srv->so._socket(param->sostate, SASOCK(&ns[q->server].addr),
q->tcp? SOCK_STREAM : SOCK_DGRAM, q->tcp? IPPROTO_TCP : IPPROTO_UDP)) == INVALID_SOCKET) continue;
#ifdef _WIN32
ioctlsocket(param->remsock, FIONBIO, &ul);
@@ -174,9 +175,9 @@ static int dnsforward(struct udploop *u, struct udpsession *ss){
fcntl(param->remsock, F_SETFL, O_NONBLOCK | fcntl(param->remsock, F_GETFL));
#endif
memset(¶m->sinsl, 0, sizeof(param->sinsl));
- *SAFAMILY(¶m->sinsl) = *SAFAMILY(&c->nservers[q->server].addr);
+ *SAFAMILY(¶m->sinsl) = *SAFAMILY(&ns[q->server].addr);
if(bindwithrange(param, param->remsock, ¶m->sinsl, param->extport)) continue;
- param->sinsr = c->nservers[q->server].addr;
+ param->sinsr = ns[q->server].addr;
ss->idle = 0;
ss->deadline = time(NULL) + param->srv->timeouts[DNS_TO];
if(q->tcp){
@@ -261,7 +262,7 @@ static int dnsprfirst(struct udploop *u, struct udpsession *ss, unsigned char *q
/* the name cache only: what it does not have the name servers are
asked, without waiting here */
else if((q->type==0x01 || q->type==0x1c) && !param->srv->s_option){
- ip = hashresolv(q->type==0x1c? &dns6_table : &dns_table, (unsigned char *)q->host, q->addr, &q->ttl);
+ ip = nscacheresolv(q->type==0x1c? AF_INET6 : AF_INET, (unsigned char *)q->host, q->addr, &q->ttl);
}
len += 5;
@@ -295,7 +296,7 @@ static int dnsprfirst(struct udploop *u, struct udpsession *ss, unsigned char *q
}
}
q->ip = ip;
- if(ip || !param->srv->config->numservers){
+ if(ip || !numservers){
param->res = dnsanswer(u, param, q, buf, len);
free(buf);
return 1;
diff --git a/src/hashtables.c b/src/hashtables.c
index 58a1d91..1b40c13 100644
--- a/src/hashtables.c
+++ b/src/hashtables.c
@@ -19,6 +19,19 @@ static void char_index2hash(const struct hashtable *ht, void *index, uint8_t *ha
}
}
+/* A host name, whose case does not matter: hashed as lower case */
+static void dns_index2hash(const struct hashtable *ht, void *index, uint8_t *hash){
+ char low[1024];
+ size_t i, len = strlen((const char *)index);
+
+ if(len >= sizeof(low)) {
+ char_index2hash(ht, index, hash);
+ return;
+ }
+ for(i = 0; i <= len; i++) low[i] = (char)tolower(((unsigned char *)index)[i]);
+ char_index2hash(ht, low, hash);
+}
+
static void param2hash_add(const struct hashtable *ht, void *index, uint8_t *hash){
struct clientparam *param = (struct clientparam *)index;
@@ -76,8 +89,11 @@ void param2hash_search(const struct hashtable *ht, void *index, uint8_t *hash){
memcpy(param->hash, hash, ht->hash_size);
}
-struct hashtable dns_table = {char_index2hash, char_index2hash, 4, 32};
-struct hashtable dns6_table = {char_index2hash, char_index2hash, 16, 32};
+struct hashtable dns_table = {dns_index2hash, dns_index2hash, 4, 32};
+struct hashtable dns6_table = {dns_index2hash, dns_index2hash, 16, 32};
+/* the hosts file, while the system's resolver is the default */
+struct hashtable hosts_table = {dns_index2hash, dns_index2hash, 4, 32};
+struct hashtable hosts6_table = {dns_index2hash, dns_index2hash, 16, 32};
struct hashtable auth_table = {param2hash_add, param2hash_search, sizeof(struct authcache), 64};
/* users: each configuration has a table of its own, made from this one */
const struct hashtable pwl_template = {char_index2hash, char_index2hash, 64, 64};
diff --git a/src/plugins.c b/src/plugins.c
index f6ee878..14af4ae 100644
--- a/src/plugins.c
+++ b/src/plugins.c
@@ -45,7 +45,7 @@ struct symbol symbols[] = {
{symbols+18, "ipauth", (void *) ipauth},
{symbols+19, "strongauth", (void *) strongauth},
{symbols+20, "checkACL", (void *) checkACL},
- {symbols+21, "nservers", (void *) confboot.nservers},
+ {symbols+21, "nservers", (void *) nservers},
{symbols+22, "udpresolve", (void *) udpresolve},
{symbols+23, "bandlim_mutex", (void *) &bandlim_mutex},
{symbols+24, "tc_mutex", (void *) &tc_mutex},
@@ -87,7 +87,7 @@ static void * findbyname(const char *name){
struct pluginlink pluginlink = {
{symbols, "", NULL},
&confboot,
- confboot.nservers,
+ nservers,
&linenum,
authfuncs,
commandhandlers,
diff --git a/src/proxy.h b/src/proxy.h
index 2d51a59..972ce1f 100644
--- a/src/proxy.h
+++ b/src/proxy.h
@@ -428,6 +428,16 @@ int parseconnusername(char *username, struct clientparam *param, int extpasswd,
int ACLmatches(struct ace* acentry, struct clientparam * param);
int checkACL(struct clientparam * param);
char * aceaction (int action);
+extern struct nserver nservers[MAXNSERVERS];
+extern int numservers;
+int getnservers(struct nserver *ns);
+int nscacheresolv(int af, unsigned char *name, unsigned char *value, uint32_t *ttl);
+void setnservers(const struct nserver *ns, int n);
+void sysdnsinit(void);
+int sysdnscount(void);
+void sysdnsapply(struct extparam *c, const struct nserver *ns, int n);
+void sysdnscheck(void);
+void confdnscommit(struct extparam *c);
uint32_t udpresolve(int af, unsigned char * name, unsigned char * value, uint32_t *retttl, struct clientparam* param, int makeauth);
struct ace * copyacl (struct ace *ac);
diff --git a/src/resolve.c b/src/resolve.c
index 4c88b5c..0be7c1d 100644
--- a/src/resolve.c
+++ b/src/resolve.c
@@ -1,5 +1,30 @@
#include "proxy.h"
+/* The name servers in force, for the process and not a configuration:
+ those of nserver lines, or else the system's. Read as a copy, they may
+ change under a resolution. */
+struct nserver nservers[MAXNSERVERS];
+int numservers = 0;
+static _3proxy_mutex_t nservers_mutex;
+
+int getnservers(struct nserver *ns){
+ int n;
+
+ _3proxy_mutex_lock(&nservers_mutex);
+ n = numservers;
+ if(ns) memcpy(ns, nservers, n * sizeof(*ns));
+ _3proxy_mutex_unlock(&nservers_mutex);
+ return n;
+}
+
+void setnservers(const struct nserver *ns, int n){
+ if(n > MAXNSERVERS) n = MAXNSERVERS;
+ _3proxy_mutex_lock(&nservers_mutex);
+ if(n) memcpy(nservers, ns, n * sizeof(*ns));
+ numservers = n;
+ _3proxy_mutex_unlock(&nservers_mutex);
+}
+
static int dnsqmatch(const unsigned char *a, const unsigned char *b, int len){
int i;
@@ -19,14 +44,12 @@ static uint32_t udpresolvec(struct extparam *c, int af, unsigned char * name, un
int i,n;
uint32_t retval;
+ struct nserver ns[MAXNSERVERS];
- if((af == AF_INET) && (retval = hashresolv(&dns_table, name, value, retttl))) {
+ if((af == AF_INET || af == AF_INET6) && (retval = nscacheresolv(af, name, value, retttl))) {
return retval;
}
- if((af == AF_INET6) && (retval = hashresolv(&dns6_table, name, value, retttl))) {
- return retval;
- }
- n = (makeauth && !SAISNULL(&c->authnserver.addr))? 1 : c->numservers;
+ n = (makeauth && !SAISNULL(&c->authnserver.addr))? 1 : getnservers(ns);
for(i=0; iauthnserver.addr);
}
else {
- usetcp = c->nservers[i].usetcp;
- *SAFAMILY(sinsl) = *SAFAMILY(&c->nservers[i].addr);
+ usetcp = ns[i].usetcp;
+ *SAFAMILY(sinsl) = *SAFAMILY(&ns[i].addr);
}
if((sock=so._socket(so.state, SASOCK(sinsl), usetcp?SOCK_STREAM:SOCK_DGRAM, usetcp?IPPROTO_TCP:IPPROTO_UDP)) == INVALID_SOCKET) break;
if(so._bind(so.state, sock,(struct sockaddr *)sinsl,SASIZE(sinsl))){
@@ -67,7 +90,7 @@ static uint32_t udpresolvec(struct extparam *c, int af, unsigned char * name, un
*sinsr = c->authnserver.addr;
}
else {
- *sinsr = c->nservers[i].addr;
+ *sinsr = ns[i].addr;
}
if(usetcp){
if(connectwithpoll(NULL, sock,(struct sockaddr *)sinsr,SASIZE(sinsr),c->timeouts[CONNECT_TO])) {
@@ -245,4 +268,360 @@ uint32_t fakeresolver (int af, unsigned char *name, unsigned char * value){
value[3] = 2;
}
return 1;
-}
\ No newline at end of file
+}
+
+/* The system's name servers and hosts file, the default for a configuration
+ without nserver (or fakeresolve): read at start, before a chroot can hide
+ them, and checked from the main loop for changes, which apply while the
+ default is in force. Hosts entries have tables of their own, emptied when
+ the default is not in force. Only the main thread works on this. */
+#define HOSTSEXPIRES ((sizeof(time_t) > 4)? (time_t)0xfffffffe : (time_t)0x7ffffffe)
+/* the time to live a hosts file name is given in an answer */
+#define HOSTSTTL 60
+#define SYSDNSCHECK 5
+
+static struct {
+ struct nserver ns[MAXNSERVERS];
+ int n; /* as last read */
+ int active; /* the default in force */
+ char *hosts; /* the hosts file as last read */
+ struct stat hsb; /* and how it was then */
+ int hok;
+ char *names; /* hosts names imported: family, name, 0 */
+ size_t namesl, namess;
+ time_t checked;
+} sysdns;
+
+/* An address of a name server, put after the others unless one of them */
+static int addns(struct nserver *ns, int n, const char *s){
+ PROXYSOCKADDRTYPE sa;
+ int i;
+
+ if(n >= MAXNSERVERS) return n;
+ memset(&sa, 0, sizeof(sa));
+ if(inet_pton(AF_INET, s, SAADDR(&sa)) > 0) *SAFAMILY(&sa) = AF_INET;
+#ifndef NOIPV6
+ else if(inet_pton(AF_INET6, s, SAADDR(&sa)) > 0) *SAFAMILY(&sa) = AF_INET6;
+#endif
+ else return n; /* a link-local one with %scope is not */
+ *SAPORT(&sa) = htons(53);
+ for(i = 0; i < n; i++)
+ if(!memcmp(&ns[i].addr, &sa, sizeof(sa))) return n;
+ memset(ns + n, 0, sizeof(*ns));
+ ns[n].addr = sa;
+ return n + 1;
+}
+
+/* Addresses in a list, separated by spaces or commas */
+static int addnslist(struct nserver *ns, int n, const char *s){
+ char a[64];
+ int l;
+
+ while(*s && n < MAXNSERVERS){
+ while(*s == ' ' || *s == ',' || *s == '\t') s++;
+ for(l = 0; *s && *s != ' ' && *s != ',' && *s != '\t'; s++) if(l < 63) a[l++] = *s;
+ a[l] = 0;
+ if(l) n = addns(ns, n, a);
+ }
+ return n;
+}
+
+#ifdef _WIN32
+/* A string value, a multi-string one as one list; 0 if there is none */
+static int regstr(HKEY k, const char *name, char *buf, DWORD size){
+ DWORD type, len = size - 2, i;
+
+ if(RegQueryValueExA(k, name, NULL, &type, (LPBYTE)buf, &len) || (type != REG_SZ && type != REG_MULTI_SZ)) return 0;
+ buf[len] = buf[len + 1] = 0;
+ if(type == REG_MULTI_SZ) for(i = 0; i + 1 < len; i++) if(!buf[i]) buf[i] = ' ';
+ return *buf != 0;
+}
+
+/* Whether an interface has an address of its own, as it does unless
+ disconnected: what DHCP gave it or the first one set */
+static int regup(HKEY k){
+ char buf[256];
+
+ if(regstr(k, "DhcpIPAddress", buf, sizeof(buf)) && strncmp(buf, "0.0.0.0", 7)) return 1;
+ return regstr(k, "IPAddress", buf, sizeof(buf)) && strncmp(buf, "0.0.0.0", 7);
+}
+
+/* The name servers of a key's interfaces: those set, or with dhcp those
+ DHCP gave; for IPv4 only of an interface with an address */
+static int regifaces(struct nserver *ns, int n, const char *path, const char *value, int needup){
+ HKEY k, ik;
+ char name[256], buf[1024];
+ DWORD i, len;
+
+ if(RegOpenKeyExA(HKEY_LOCAL_MACHINE, path, 0, KEY_READ, &k)) return n;
+ for(i = 0; n < MAXNSERVERS; i++){
+ len = sizeof(name);
+ if(RegEnumKeyExA(k, i, name, &len, NULL, NULL, NULL, NULL)) break;
+ if(RegOpenKeyExA(k, name, 0, KEY_READ, &ik)) continue;
+ if((!needup || regup(ik)) && regstr(ik, value, buf, sizeof(buf))) n = addnslist(ns, n, buf);
+ RegCloseKey(ik);
+ }
+ RegCloseKey(k);
+ return n;
+}
+
+/* As Windows has them: set for the system, set for an interface, then
+ those DHCP gave */
+static int readsysns(struct nserver *ns){
+ HKEY k;
+ char buf[1024];
+ int n = 0;
+ static const char tcpip[] = "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters";
+
+ if(!RegOpenKeyExA(HKEY_LOCAL_MACHINE, tcpip, 0, KEY_READ, &k)){
+ if(regstr(k, "NameServer", buf, sizeof(buf))) n = addnslist(ns, n, buf);
+ RegCloseKey(k);
+ }
+ n = regifaces(ns, n, "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces", "NameServer", 1);
+#ifndef NOIPV6
+ n = regifaces(ns, n, "SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters\\Interfaces", "NameServer", 0);
+#endif
+ n = regifaces(ns, n, "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces", "DhcpNameServer", 1);
+ if(n < MAXNSERVERS && !RegOpenKeyExA(HKEY_LOCAL_MACHINE, tcpip, 0, KEY_READ, &k)){
+ if(regstr(k, "DhcpNameServer", buf, sizeof(buf))) n = addnslist(ns, n, buf);
+ RegCloseKey(k);
+ }
+ return n;
+}
+
+static const char *hostspath(void){
+ static char path[MAX_PATH + 32];
+ UINT l;
+
+ if(!*path){
+ l = GetSystemDirectoryA(path, MAX_PATH);
+ if(!l || l >= MAX_PATH) strcpy(path, "C:\\Windows\\System32");
+ strcat(path, "\\drivers\\etc\\hosts");
+ }
+ return path;
+}
+#else
+/* As resolv.conf has them, in its order */
+static int readsysns(struct nserver *ns){
+ FILE *f;
+ char line[512], *s, *e;
+ int n = 0;
+
+ if(!(f = fopen("/etc/resolv.conf", "r"))) return 0;
+ while(n < MAXNSERVERS && fgets(line, sizeof(line), f)){
+ if(strncmp(line, "nameserver", 10) || (line[10] != ' ' && line[10] != '\t')) continue;
+ for(s = line + 10; *s == ' ' || *s == '\t'; s++);
+ for(e = s; *e && !isspace((unsigned char)*e) && *e != '#' && *e != ';'; e++);
+ *e = 0;
+ n = addns(ns, n, s);
+ }
+ fclose(f);
+ return n;
+}
+
+static const char *hostspath(void){
+ return "/etc/hosts";
+}
+#endif
+
+static char *readhosts(struct stat *sb){
+ FILE *f;
+ char *buf;
+ long l;
+
+ if(stat(hostspath(), sb) || !(f = fopen(hostspath(), "rb"))) return NULL;
+ fseek(f, 0, SEEK_END);
+ l = ftell(f);
+ fseek(f, 0, SEEK_SET);
+ if(l < 0 || l > 64*1024*1024 || !(buf = malloc((size_t)l + 1))){
+ fclose(f);
+ return NULL;
+ }
+ l = (long)fread(buf, 1, (size_t)l, f);
+ buf[l] = 0;
+ fclose(f);
+ return buf;
+}
+
+/* The hosts names imported before taken out */
+static void hostsremove(void){
+ size_t i;
+
+ for(i = 0; i < sysdns.namesl; i += strlen(sysdns.names + i + 1) + 2)
+ hashdelete(sysdns.names[i] == 6? &hosts6_table : &hosts_table, sysdns.names + i + 1);
+ sysdns.namesl = 0;
+}
+
+static void hostsname(int af, const char *name){
+ size_t l = strlen(name) + 2;
+ char *p;
+
+ if(sysdns.namesl + l > sysdns.namess){
+ size_t ns = (sysdns.namess + l) * 2;
+
+ if(!(p = realloc(sysdns.names, ns))) return;
+ sysdns.names = p;
+ sysdns.namess = ns;
+ }
+ sysdns.names[sysdns.namesl] = (char)af;
+ memcpy(sysdns.names + sysdns.namesl + 1, name, l - 1);
+ sysdns.namesl += l;
+}
+
+/* The hosts file imported: its first address of a family for a name, as
+ the C library takes it */
+static void hostsimport(void){
+ char *s, *e, *next, *tok, addr[16], name[256];
+ struct hashtable *ht;
+ unsigned char old[16];
+ uint32_t ttl;
+ int af, l;
+
+ hostsremove();
+ if(!sysdns.hosts) return;
+ for(s = sysdns.hosts; *s; s = next){
+ for(e = s; *e && *e != '\n' && *e != '\r' && *e != '#'; e++);
+ for(next = e; *next && *next != '\n'; next++);
+ if(*next) next++;
+ /* the address */
+ while(s < e && (*s == ' ' || *s == '\t')) s++;
+ for(tok = s; s < e && *s != ' ' && *s != '\t'; s++);
+ if((l = (int)(s - tok)) <= 0 || l >= (int)sizeof(name)) continue;
+ memcpy(name, tok, l);
+ name[l] = 0;
+ if(inet_pton(AF_INET, name, addr) > 0) af = 4;
+#ifndef NOIPV6
+ else if(inet_pton(AF_INET6, name, addr) > 0) af = 6;
+#endif
+ else continue;
+ ht = (af == 6)? &hosts6_table : &hosts_table;
+ /* its names */
+ for(;;){
+ while(s < e && (*s == ' ' || *s == '\t')) s++;
+ if(s >= e) break;
+ for(tok = s; s < e && *s != ' ' && *s != '\t'; s++);
+ if((l = (int)(s - tok)) >= (int)sizeof(name)) continue;
+ for(l = 0; tok + l < s; l++) name[l] = (char)tolower((unsigned char)tok[l]);
+ name[l] = 0;
+ /* one before in the file */
+ if(hashresolv(ht, name, old, &ttl)) continue;
+ hashadd(ht, name, addr, HOSTSEXPIRES);
+ hostsname(af, name);
+ }
+ }
+}
+
+/* The system's name servers and hosts file read again; what cannot be read
+ now (after a chroot) or has no name server stays as it was. Returns 1 for
+ name servers changed, 2 for the hosts file, as a mask. */
+static int sysdnsread(void){
+ struct nserver ns[MAXNSERVERS];
+ struct stat sb;
+ char *h;
+ int n, res = 0;
+
+ memset(ns, 0, sizeof(ns));
+ n = readsysns(ns);
+ if(n && (n != sysdns.n || memcmp(ns, sysdns.ns, n * sizeof(*ns)))){
+ memcpy(sysdns.ns, ns, sizeof(ns));
+ sysdns.n = n;
+ res |= 1;
+ }
+ if(!stat(hostspath(), &sb) && (!sysdns.hok || sb.st_mtime != sysdns.hsb.st_mtime || sb.st_size != sysdns.hsb.st_size
+#ifndef _WIN32
+ || sb.st_ino != sysdns.hsb.st_ino || sb.st_ctime != sysdns.hsb.st_ctime
+#endif
+ ) && (h = readhosts(&sb))){
+ free(sysdns.hosts);
+ sysdns.hosts = h;
+ sysdns.hsb = sb;
+ sysdns.hok = 1;
+ res |= 2;
+ }
+ return res;
+}
+
+/* Read at start, before the configuration and a chroot in it, and in force
+ while it is read: its host names are resolved with them, nserver lines
+ taking their place once it is committed */
+void sysdnsinit(void){
+ _3proxy_mutex_init(&nservers_mutex);
+ inithashtable(&hosts_table, 256, 256, 1 << 22);
+#ifndef NOIPV6
+ inithashtable(&hosts6_table, 256, 256, 1 << 22);
+#endif
+ sysdnsread();
+ sysdns.checked = time(NULL);
+ if(!sysdns.n) return;
+ sysdns.active = 1;
+ setnservers(sysdns.ns, sysdns.n);
+ resolvfunc = myresolver;
+ hostsimport();
+}
+
+/* A name known without asking: one nsrecord made first, then the hosts
+ file while it is in force, then what name servers answered */
+int nscacheresolv(int af, unsigned char *name, unsigned char *value, uint32_t *ttl){
+ struct hashtable *cache = (af == AF_INET6)? &dns6_table : &dns_table;
+ uint32_t t = 0;
+ int r;
+
+ r = hashresolv(cache, name, value, &t);
+ if(r && t > 3600*24*365){
+ if(ttl) *ttl = t;
+ return r;
+ }
+ if(sysdns.active && hashresolv((af == AF_INET6)? &hosts6_table : &hosts_table, name, value, NULL)){
+ if(ttl) *ttl = HOSTSTTL;
+ return 1;
+ }
+ if(r && ttl) *ttl = t;
+ return r;
+}
+
+int sysdnscount(void){
+ return sysdns.n;
+}
+
+/* A configuration committed: the name servers of its nserver lines, else,
+ with nothing else to resolve names, the system's and its hosts file, read
+ again. Until then those in force stay. */
+void sysdnsapply(struct extparam *c, const struct nserver *ns, int n){
+ sysdns.active = 0;
+ if(n){
+ setnservers(ns, n);
+ hostsremove();
+ return;
+ }
+ if(c->resolvfunc){
+ setnservers(NULL, 0);
+ hostsremove();
+ return;
+ }
+ sysdnsread();
+ sysdns.checked = curtime;
+ sysdns.active = 1;
+ setnservers(sysdns.ns, sysdns.n);
+ if(!sysdns.n) return; /* the C library, as before */
+ c->resolvfunc = myresolver;
+ hostsimport();
+}
+
+/* From the main loop: name servers and hosts file read again while the
+ default is in force, every few seconds. What cannot be read (after a
+ chroot) is left as it was. */
+void sysdnscheck(void){
+ int was = sysdns.n, res;
+
+ if(!sysdns.active || curtime - sysdns.checked < SYSDNSCHECK) return;
+ sysdns.checked = curtime;
+ res = sysdnsread();
+ if(res & 1){
+ setnservers(sysdns.ns, sysdns.n);
+ dolog(NULL, (unsigned char *)"System name servers changed");
+ /* none before: the C library resolved until now */
+ if(!was) conf->resolvfunc = resolvfunc = myresolver;
+ }
+ if(resolvfunc == myresolver && ((res & 2) || !was)) hostsimport();
+ if(res & 2) dolog(NULL, (unsigned char *)"Hosts file changed");
+}
diff --git a/src/structures.h b/src/structures.h
index 10df1d5..43c4eba 100644
--- a/src/structures.h
+++ b/src/structures.h
@@ -502,7 +502,7 @@ struct nserver {
int usetcp;
};
-#define MAXNSERVERS 5
+#define MAXNSERVERS 3
#define MAXRADIUS 5
struct radserver {
@@ -919,8 +919,7 @@ struct extparam {
time_t logtime;
unsigned logdumpsrv, logdumpcli;
char delimchar;
- struct nserver nservers[MAXNSERVERS], authnserver;
- int numservers;
+ struct nserver authnserver;
struct radserver radiuslist[MAXRADIUS];
int nradservers;
char radiussecret[64];
@@ -999,6 +998,8 @@ struct child {
extern struct hashtable dns_table;
extern struct hashtable dns6_table;
+extern struct hashtable hosts_table;
+extern struct hashtable hosts6_table;
extern struct hashtable auth_table;
extern const struct hashtable pwl_template;
diff --git a/tests/cases/sysdns.py b/tests/cases/sysdns.py
new file mode 100644
index 0000000..e4b949b
--- /dev/null
+++ b/tests/cases/sysdns.py
@@ -0,0 +1,83 @@
+"""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")