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")