diff --git a/src/dnspr.c b/src/dnspr.c index 44b1ec5..87dab92 100644 --- a/src/dnspr.c +++ b/src/dnspr.c @@ -15,22 +15,30 @@ #define BUFSIZE 16384 /* dnspr: a query is a session of its own in the service's thread (see - udploop() in proxymain.c). It is answered there and then, from the name - cache, the resolver or a fake address, or sent on to the first name - server, whose answer, or its timeout, ends it. A name server over TCP is - still waited for in the loop. */ + udploop() in mainudp.c). It is answered there and then from a fake + address, the name cache, or the service's own address for a PTR; + anything else goes to the name servers, one after another, over UDP or + TCP, the loop waiting for their sockets: nothing here blocks. An address + in an answer is kept in the name cache. */ struct dnsquery { unsigned type; uint32_t ip, ttl; unsigned char addr[16]; char *host; - unsigned char *query; /* as the client sent it, for a name server */ + /* the query as the client sent it, behind 2 bytes for its length over + TCP */ + unsigned char *query; int len; + /* the name server asked, and over TCP what of its answer came */ + int server; + int tcp, connecting; + unsigned char *answer; + int got; }; /* The answer to the client, the query in buf made one: ip set, an address - for it; a PTR for the service's own address; else the name not found. */ + for it; else the name not found. */ static int dnsanswer(struct udploop *u, struct clientparam *param, struct dnsquery *q, unsigned char *buf, int len){ uint32_t ttl; @@ -59,6 +67,113 @@ static int dnsanswer(struct udploop *u, struct clientparam *param, struct dnsque return q->ip? 0 : 888; } +/* An address of the type asked for in a name server's answer: noted for the + log and kept in the name cache for its TTL, up to 12 hours. */ +static void dnscache(struct clientparam *param, struct dnsquery *q, unsigned char *buf, int len){ + int k = 12, n, j, rlen; + unsigned type; + uint32_t ttl; + + if((q->type != 0x01 && q->type != 0x1c) || !q->host) return; + while(k < len && buf[k]) k += buf[k] + 1; + k += 5; + n = buf[7] + (((unsigned)buf[6])<<8); + for(j = 0; j < n && k < len; j++){ + /* the name: a pointer, or labels to the end */ + if((buf[k] & 0xc0) == 0xc0) k += 2; + else { + while(k < len && buf[k]) k += buf[k] + 1; + k++; + } + if(k + 10 > len) return; + type = buf[k+1] + (((unsigned)buf[k])<<8); + rlen = buf[k+9] + (((unsigned)buf[k+8])<<8); + if(k + 10 + rlen > len) return; + if(type == q->type && rlen == (q->type == 0x01? 4 : 16)){ + ttl = ntohl(*(uint32_t *)(buf + k + 4)); + if(ttl > 3600*12) ttl = 3600*12; + if(!ttl) ttl = 1; + memcpy(q->addr, buf + k + 10, rlen); + q->ip = 1; + if(!param->srv->s_option) + hashadd(q->type == 0x1c? &dns6_table : &dns_table, (unsigned char *)q->host, q->addr, curtime + ttl); + return; + } + k += 10 + rlen; + } +} + +/* An answer from a name server: to the client as it is if it has answers, + else as the name not found. 1: the query is done. */ +static int dnsreply(struct udploop *u, struct udpsession *ss, unsigned char *buf, int len){ + struct clientparam *param = ss->param; + struct dnsquery *q = ss->data; + + param->statssrv64 += len; + param->nreads++; + if(buf[6] || buf[7]){ + dnscache(param, q, buf, len); + param->res = (udpanswer(u, param, buf, len) == len)? 0 : 822; + return 1; + } + q->ip = 0; + param->res = dnsanswer(u, param, q, buf, len); + return 1; +} + +/* The query to name server q->server or a later one: 0 once it is on its + way, the loop to wait for the answer; nonzero if none takes it. */ +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; +#ifdef _WIN32 + unsigned long ul = 1; +#endif + + for(; q->server < c->numservers; q->server++){ + if(param->remsock != INVALID_SOCKET){ + param->srv->so._closesocket(param->sostate, param->remsock); + param->remsock = INVALID_SOCKET; + } + q->tcp = c->nservers[q->server].usetcp; + q->got = 0; + if((param->remsock = param->srv->so._socket(param->sostate, SASOCK(&c->nservers[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); +#else + 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); + if(bindwithrange(param, param->remsock, ¶m->sinsl, param->extport)) continue; + param->sinsr = c->nservers[q->server].addr; + ss->idle = 0; + ss->deadline = time(NULL) + param->srv->timeouts[DNS_TO]; + if(q->tcp){ + /* connected, then written to, as the loop sees it writable */ + if(param->srv->so._connect(param->sostate, param->remsock, (struct sockaddr *)¶m->sinsr, SASIZE(¶m->sinsr)) +#ifdef _WIN32 + && WSAGetLastError() != WSAEWOULDBLOCK +#else + && errno != EINPROGRESS +#endif + ) continue; + q->connecting = 1; + ss->events = POLLOUT; + return 0; + } + ss->events = POLLIN; + if(param->srv->so._sendto(param->sostate, param->remsock, (char *)q->query + 2, q->len, 0, + (struct sockaddr *)¶m->sinsr, SASIZE(¶m->sinsr)) != q->len) continue; + param->statscli64 += q->len; + param->nwrites++; + return 0; + } + return 1; +} + static int dnsprfirst(struct udploop *u, struct udpsession *ss, unsigned char *query, int i){ struct clientparam *param = ss->param; struct dnsquery *q; @@ -66,9 +181,6 @@ static int dnsprfirst(struct udploop *u, struct udpsession *ss, unsigned char *q unsigned char c; int res, len; uint32_t ttl, ip = 0; -#ifdef _WIN32 - unsigned long ul = 1; -#endif if(!(q = calloc(1, sizeof(*q))) || !(q->query = malloc(BUFSIZE + 2))){ free(q); @@ -78,21 +190,23 @@ static int dnsprfirst(struct udploop *u, struct udpsession *ss, unsigned char *q if(i > BUFSIZE) i = BUFSIZE; memcpy(q->query + 2, query, i); q->len = i; - buf = q->query + 2; - param->sincl = param->srv->intsa; if(i<=13 || i>1000) return (param->res = 814); + /* the query as it came is what goes to a name server; buf is worked on */ + if(!(buf = malloc(BUFSIZE))) return (param->res = 21); + memcpy(buf, query, i); buf[BUFSIZE - 1] = 0; + param->sincl = param->srv->intsa; param->operation = DNSRESOLVE; - if((res = (*param->srv->authfunc)(param))) return (param->res = res); + if((res = (*param->srv->authfunc)(param))) { free(buf); return (param->res = res); } - if(buf[4]!=0 || buf[5]!=1) return (param->res = 816); + if(buf[4]!=0 || buf[5]!=1) { free(buf); return (param->res = 816); } for(len = 12; len (i-4)) return (param->res = 817); - if(!(q->host = strdup((char *)buf+13))) return (param->res = 21); + if(len > (i-4)) { free(buf); return (param->res = 817); } + if(!(q->host = strdup((char *)buf+13))) { free(buf); return (param->res = 21); } for(s2 = buf + 12; (s1 = (unsigned char *)strchr((char *)s2 + 1, '.')); s2 = s1)*s2 = (unsigned char)((s1 - s2) - 1); *s2 = (len - (int)(s2 - buf)) - 1; @@ -108,8 +222,10 @@ static int dnsprfirst(struct udploop *u, struct udpsession *ss, unsigned char *q memcpy(q->addr, param->srv->fakeip6, 16); } #endif + /* 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 = udpresolve((q->type==0x1c)?AF_INET6:AF_INET, (unsigned char *)q->host, q->addr, &q->ttl, param, 0); + ip = hashresolv(q->type==0x1c? &dns6_table : &dns_table, (unsigned char *)q->host, q->addr, &q->ttl); } len += 5; @@ -137,85 +253,73 @@ static int dnsprfirst(struct udploop *u, struct udpsession *ss, unsigned char *q memcpy(buf+len+13,(void *)"3proxy",6); len += 20; q->ip = 1; - if(udpanswer(u, param, buf, len) != len) return (param->res = 819); + param->res = (udpanswer(u, param, buf, len) == len)? 0 : 819; + free(buf); return 1; } } q->ip = ip; if(ip || !param->srv->config->numservers){ param->res = dnsanswer(u, param, q, buf, len); + free(buf); return 1; } - - /* on to the first name server */ - if((param->remsock=param->srv->so._socket(param->sostate, SASOCK(¶m->srv->config->nservers[0].addr), param->srv->config->nservers[0].usetcp? SOCK_STREAM:SOCK_DGRAM, param->srv->config->nservers[0].usetcp?IPPROTO_TCP:IPPROTO_UDP)) == INVALID_SOCKET) - return (param->res = 818); - memset(¶m->sinsl, 0, sizeof(param->sinsl)); - *SAFAMILY(¶m->sinsl) = *SAFAMILY(¶m->srv->config->nservers[0].addr); - if(bindwithrange(param, param->remsock, ¶m->sinsl, param->extport)) return (param->res = 819); - param->sinsr = param->srv->config->nservers[0].addr; - if(param->srv->config->nservers[0].usetcp) { - unsigned short us; - - if(connectwithpoll(param, param->remsock,(struct sockaddr *)¶m->sinsr,SASIZE(¶m->sinsr),param->srv->timeouts[CONNECT_TO])) return (param->res = 830); - buf -= 2; - *(unsigned short*)buf = htons(i); - i += 2; - if(socksend(param, param->remsock, buf, i, param->srv->timeouts[SINGLEBYTE_L]) != i) return (param->res = 820); - param->statscli64 += i; - param->nwrites++; - len = sockrecvfrom(param, param->remsock, NULL, buf, BUFSIZE, param->srv->timeouts[DNS_TO]*1000); - if(len <= 13) return (param->res = 821); - param->statssrv64 += len; - param->nreads++; - us = ntohs(*(unsigned short *)buf); - if(us > 4096) return (param->res = 833); - buf += 2; - len -= 2; - if(len < us) len += sockgetlinebuf(param, SERVER, buf+len, us - len, 0, param->srv->timeouts[SINGLEBYTE_L]); - if(len != us) return (param->res = 832); - if(buf[6] || buf[7]){ - if(udpanswer(u, param, buf, len) != len) return (param->res = 822); - return 1; - } - param->res = dnsanswer(u, param, q, buf, len); - return 1; - } -#ifdef _WIN32 - ioctlsocket(param->remsock, FIONBIO, &ul); -#else - fcntl(param->remsock, F_SETFL, O_NONBLOCK | fcntl(param->remsock, F_GETFL)); -#endif - if(param->srv->so._sendto(param->sostate, param->remsock, (char *)buf, i, 0, (struct sockaddr *)¶m->sinsr, SASIZE(¶m->sinsr)) != i) - return (param->res = 820); - param->statscli64 += i; - param->nwrites++; - /* its answer is waited for in the loop, as long as DNS_TO */ - ss->idle = 0; - ss->deadline = time(NULL) + param->srv->timeouts[DNS_TO]; + free(buf); + if(dnsforward(u, ss)) return (param->res = 818); return 0; } -/* The name server's answer: to the client as it is if it has answers, - else as the name not found. */ +/* The name server's socket: connected, over TCP, so the query is written; + an answer, or over TCP some of it. */ static int dnsprserver(struct udploop *u, struct udpsession *ss){ struct clientparam *param = ss->param; struct dnsquery *q = ss->data; int len; + if(q->tcp && q->connecting){ + q->connecting = 0; + q->query[0] = (unsigned char)(q->len >> 8); + q->query[1] = (unsigned char)(q->len & 0xff); + if(param->srv->so._send(param->sostate, param->remsock, (char *)q->query, q->len + 2, 0) != q->len + 2){ + q->server++; + return dnsforward(u, ss)? (param->res = 820) : 0; + } + param->statscli64 += q->len + 2; + param->nwrites++; + ss->events = POLLIN; + return 0; + } + if(q->tcp){ + unsigned short us; + + if(!q->answer && !(q->answer = malloc(4096 + 2))) return (param->res = 21); + len = param->srv->so._recv(param->sostate, param->remsock, (char *)q->answer + q->got, 4096 + 2 - q->got, 0); + if(len < 0 && (errno == EAGAIN || errno == EINTR)) return 0; + if(len <= 0){ + q->server++; + return dnsforward(u, ss)? (param->res = 821) : 0; + } + q->got += len; + if(q->got < 2) return 0; + us = (unsigned short)((q->answer[0] << 8) | q->answer[1]); + if(us > 4096 || us <= 13) return (param->res = 833); + if(q->got < us + 2) return 0; + return dnsreply(u, ss, q->answer + 2, us); + } len = param->srv->so._recvfrom(param->sostate, param->remsock, (char *)u->buf, UDPBUFSIZE, 0, NULL, NULL); if(len < 0 && (errno == EAGAIN || errno == EINTR)) return 0; - if(len <= 13) return (param->res = 821); - param->statssrv64 += len; - param->nreads++; - if(u->buf[6] || u->buf[7]){ - if(udpanswer(u, param, u->buf, len) != len) return (param->res = 822); - param->res = 0; - return 1; - } - q->ip = 0; - param->res = dnsanswer(u, param, q, u->buf, len); - return 1; + /* an answer to another query, or none at all */ + if(len <= 13 || u->buf[0] != q->query[2] || u->buf[1] != q->query[3]) return 0; + return dnsreply(u, ss, u->buf, len); +} + +/* No answer in DNS_TO: the next name server, if there is one. */ +static int dnsprtimeout(struct udploop *u, struct udpsession *ss){ + struct dnsquery *q = ss->data; + + q->server++; + if(dnsforward(u, ss)) return (ss->param->res = 821); + return 0; } static void dnsprend(struct udploop *u, struct udpsession *ss){ @@ -233,6 +337,7 @@ static void dnsprend(struct udploop *u, struct udpsession *ss){ if(q){ free(q->host); free(q->query); + free(q->answer); free(q); } ss->data = NULL; @@ -244,5 +349,6 @@ const struct udpservice dnsprservice = { NULL, dnsprserver, dnsprend, - 821 + 821, + dnsprtimeout }; diff --git a/src/mainudp.c b/src/mainudp.c index 5b5682d..38f7542 100644 --- a/src/mainudp.c +++ b/src/mainudp.c @@ -344,7 +344,7 @@ static int udploop(struct srvstart *s){ timeo = ss->deadline > now? (int)(ss->deadline - now) * 1000 : 0; if(ss->param->remsock != INVALID_SOCKET){ st.fds[nfds].fd = ss->param->remsock; - st.fds[nfds].events = POLLIN; + st.fds[nfds].events = ss->events? ss->events : POLLIN; st.fds[nfds].revents = 0; st.of[nfds++] = ss; } @@ -375,8 +375,11 @@ static int udploop(struct srvstart *s){ for(ss = st.head; ss; ss = next){ next = ss->next; if(!ss->done && ss->deadline && ss->deadline <= now){ - ss->done = 1; - ss->param->res = st.svc->timeoutres; + if(st.svc->timeout) ss->done = (*st.svc->timeout)(&st.u, ss); + else { + ss->done = 1; + ss->param->res = st.svc->timeoutres; + } } if(ss->done){ udpremove(&st, ss); diff --git a/src/proxy.h b/src/proxy.h index fac3859..583ba4b 100644 --- a/src/proxy.h +++ b/src/proxy.h @@ -252,6 +252,7 @@ struct udpsession { int idle; /* seconds of silence it lasts, 0: deadline fixed */ int keyed; /* found by its client's address */ int done; /* to be ended, param->res saying how */ + short events; /* polled for on remsock, 0: POLLIN */ void *data; /* the service's own, freed by it in end() */ }; struct udpservice { @@ -269,6 +270,9 @@ struct udpservice { void (*end)(struct udploop *u, struct udpsession *ss); /* param->res of a session whose deadline passed */ int timeoutres; + /* or, if set, its deadline passed: 0 if it goes on, a new deadline + set, nonzero to end it, param->res saying how */ + int (*timeout)(struct udploop *u, struct udpsession *ss); }; struct udploop { struct srvparam *srv; diff --git a/src/udppm.c b/src/udppm.c index 082628d..8e2a43d 100644 --- a/src/udppm.c +++ b/src/udppm.c @@ -109,7 +109,8 @@ const struct udpservice udppmservice = { udppmclient, udppmserver, udppmend, - 92 + 92, + NULL }; #ifdef WITHMAIN diff --git a/tests/cases/udploop.py b/tests/cases/udploop.py index fa4850a..2b9be80 100644 --- a/tests/cases/udploop.py +++ b/tests/cases/udploop.py @@ -24,28 +24,64 @@ def dns_raw(name, qtype, qid=0x3B3B): return q + b"\x00" + struct.pack("!HH", qtype, 1) -def fake_name_server(answer): - """A name server answering every query with one TXT record, the text - given; and one which answers nothing.""" - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +def dns_reply(data, answer): + """The answer to a query: an A record 10.9.8.7 for an A query, else a TXT + record with the text given.""" + reply = bytearray(data) + reply[2] = 0x81 + reply[3] = 0x80 + reply[6:8] = struct.pack("!H", 1) + pos = 12 + while data[pos]: + pos += data[pos] + 1 + qtype = struct.unpack("!H", data[pos + 1:pos + 3])[0] + if qtype == 1: + reply += b"\xc0\x0c" + struct.pack("!HHIH", 1, 1, 60, 4) + bytes([10, 9, 8, 7]) + else: + txt = bytes([len(answer)]) + answer + reply += b"\xc0\x0c" + struct.pack("!HHIH", 16, 1, 60, len(txt)) + txt + return bytes(reply) + + +def fake_name_server(answer, tcp=False): + """A name server answering every query (see dns_reply), over UDP or + TCP; with answer None, one which answers nothing.""" + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM if tcp else socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(("127.0.0.1", 0)) + def serve_tcp(c): + try: + c.settimeout(10) + head = c.recv(2) + n = struct.unpack("!H", head)[0] + data = b"" + while len(data) < n: + data += c.recv(n - len(data)) + reply = dns_reply(data, answer) + # in two pieces, as a stream may bring it + c.sendall(struct.pack("!H", len(reply)) + reply[:5]) + time.sleep(0.05) + c.sendall(reply[5:]) + except OSError: + pass + c.close() + def serve(): while True: try: + if tcp: + c, _ = sock.accept() + threading.Thread(target=serve_tcp, args=(c,), daemon=True).start() + continue data, peer = sock.recvfrom(65536) except OSError: return - if answer is None: - continue - reply = bytearray(data) - reply[2] = 0x81 - reply[3] = 0x80 - reply[6:8] = struct.pack("!H", 1) - txt = bytes([len(answer)]) + answer - reply += b"\xc0\x0c" + struct.pack("!HHIH", 16, 1, 60, len(txt)) + txt - sock.sendto(bytes(reply), peer) + if answer is not None: + sock.sendto(dns_reply(data, answer), peer) + if tcp: + sock.listen(8) threading.Thread(target=serve, daemon=True).start() return sock, sock.getsockname()[1] @@ -201,9 +237,22 @@ def run(t): if got and b"from-the-server" in got else \ t.fail("a TXT query is sent on to the name server and its answer relayed", "the TXT", got) + # an address not in the cache is asked of the name server, and kept + got = exchange(s, fwd, dns_raw("a.test", 1)) + t.ok("an A query not in the cache is answered by the name server") \ + if got and bytes([10, 9, 8, 7]) in got else \ + t.fail("an A query not in the cache is answered by the name server", "10.9.8.7", got) + good.close() + time.sleep(0.2) + got = exchange(s, fwd, dns_raw("a.test", 1)) + t.ok("and then from the name cache, the name server gone") \ + if got and bytes([10, 9, 8, 7]) in got else \ + t.fail("and then from the name cache, the name server gone", "10.9.8.7", got) + t.wait_udp(slow, payload=dns_raw("local.test", 1)) pending = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - pending.sendto(dns_raw("mute.test", 16), ("127.0.0.1", slow)) + # an A query, which the resolver used to wait for in place + pending.sendto(dns_raw("mute.test", 1), ("127.0.0.1", slow)) start = time.time() other = exchange(s, slow, dns_raw("local.test", 1), timeout=2) t.ok(f"while one query waits for a silent name server another is answered ({time.time() - start:.2f} s)") \ @@ -213,5 +262,41 @@ def run(t): while time.time() < deadline and " 00821 " not in dns2.output(): time.sleep(0.2) t.contains(dns2.output(), " 00821 ", "the waiting one ends at DNS_TO (821)") - for x in (s, pending, good, mute): + + # a name server over TCP; and a second one asked when the first is silent + tcpns, tport = fake_name_server(b"over-tcp", tcp=True) + second, sport = fake_name_server(b"the-second") + viatcp, fallback = t.free_port(), t.free_port() + t.start("udploop_tcpns", f""" + log + timeouts 1 5 30 60 180 1800 2 60 15 5 5 + nserver 127.0.0.1:{tport}/tcp + nscache 256 + nsrecord local.test 10.1.2.3 + auth iponly + allow * + dnspr -p{viatcp} + """) + t.start("udploop_fallback", f""" + log + timeouts 1 5 30 60 180 1800 2 60 15 5 5 + nserver 127.0.0.1:{mport} + nserver 127.0.0.1:{sport} + nscache 256 + nsrecord local.test 10.1.2.3 + auth iponly + allow * + dnspr -p{fallback} + """) + t.wait_udp(viatcp, payload=dns_raw("local.test", 1)) + got = exchange(s, viatcp, dns_raw("tcp.test", 16), timeout=4) + t.ok("a name server over TCP is asked, its answer in pieces put together") \ + if got and b"over-tcp" in got else \ + t.fail("a name server over TCP is asked, its answer in pieces put together", "the TXT", got) + t.wait_udp(fallback, payload=dns_raw("local.test", 1)) + got = exchange(s, fallback, dns_raw("second.test", 16), timeout=6) + t.ok("the first name server silent, the second is asked after DNS_TO") \ + if got and b"the-second" in got else \ + t.fail("the first name server silent, the second is asked after DNS_TO", "the TXT", got) + for x in (s, pending, mute, tcpns, second): x.close()