diff --git a/src/sockmap.c b/src/sockmap.c index 99e017d..4c60428 100644 --- a/src/sockmap.c +++ b/src/sockmap.c @@ -75,6 +75,8 @@ int sockmap(struct clientparam * param, int timeo, int usesplice){ #endif int HASERROR=0; int CLIENTTERMREAD = 0, CLIENTTERMWRITE = 0, SERVERTERMREAD = 0, SERVERTERMWRITE = 0; + /* the side has hung up: closed by the peer after its own was shut down */ + int CLIENTHUP = 0, SERVERHUP = 0; int after = 0; struct pollfd fds[8]; int fdsc = 0; @@ -629,6 +631,9 @@ log("done read from server to buf"); #endif )) cli_events |= POLLOUT; + /* poll() reports a hang-up whatever is asked, and would + return at once every time: wait for the other side then */ + if(!cli_events && CLIENTHUP) fds[fdsc].fd = INVALID_SOCKET; if(cli_events){ fds[fdsc].fd = param->clisock; fds[fdsc].events = cli_events; @@ -664,9 +669,13 @@ log("ready to write to client"); if(fds[fdsc].events & POLLOUT) CLIENTTERMWRITE = 1; } } - } else if(fds[fdsc].revents & (POLLERR|POLLNVAL|POLLHUP)) { + } else if(fds[fdsc].revents & (POLLERR|POLLNVAL)) { CLIENTTERMREAD = 1; CLIENTTERMWRITE = 1; + } else if(fds[fdsc].revents & POLLHUP) { + /* what the client sent before it closed is still to be + read, once the buffer has room for it */ + CLIENTHUP = 1; } fdsc++; @@ -682,6 +691,7 @@ log("ready to write to client"); #endif )) srv_events |= POLLOUT; + if(!srv_events && SERVERHUP) fds[fdsc].fd = INVALID_SOCKET; if(srv_events){ fds[fdsc].fd = param->remsock; fds[fdsc].events = srv_events; @@ -724,9 +734,12 @@ log("server terminated connection"); if(fds[fdsc].events & POLLOUT) SERVERTERMWRITE = 1; } } - } else if(fds[fdsc].revents & (POLLERR|POLLNVAL|POLLHUP)) { + } else if(fds[fdsc].revents & (POLLERR|POLLNVAL)) { SERVERTERMREAD = 1; SERVERTERMWRITE = 1; + } else if(fds[fdsc].revents & POLLHUP) { + /* the rest of the answer may still be waiting to be read */ + SERVERHUP = 1; } fdsc++; // } diff --git a/tests/cases/portmap.py b/tests/cases/portmap.py index 72be8f7..d456192 100644 --- a/tests/cases/portmap.py +++ b/tests/cases/portmap.py @@ -1,5 +1,29 @@ """The port mappers: tcppm forwards a TCP port, udppm a UDP one.""" +import socket +import threading +import time + +HALFCLOSE_SIZE = 20000000 + + +def halfclose_origin(listener): + """Answers each connection which asks for something with HALFCLOSE_SIZE + bytes, then waits for the client's end of the stream and closes. The + connections waiting for the service to start ask nothing.""" + while True: + try: + c, _ = listener.accept() + except OSError: + return + try: + if c.recv(1000): + c.sendall(b"x" * HALFCLOSE_SIZE) + c.recv(10) + except OSError: + pass + c.close() + def run(t): # --- tcppm --------------------------------------------------------- @@ -40,6 +64,41 @@ def run(t): t.stop_all() + # A client which shuts down its sending side still gets the whole answer, + # even when it reads slowly: the server has finished and closed by then, + # and the rest of the answer waits in the proxy's buffer and the kernel's. + listener = socket.socket() + listener.bind(("127.0.0.1", 0)) + listener.listen(1) + threading.Thread(target=halfclose_origin, args=(listener,), daemon=True).start() + halfclosed = t.free_port() + t.start("portmap_halfclose", f""" + log + auth none + tcppm {halfclosed} 127.0.0.1 {listener.getsockname()[1]} + """, ports=[halfclosed]) + s = socket.socket() + s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 32768) + s.settimeout(30) + got = 0 + try: + s.connect(("127.0.0.1", halfclosed)) + s.sendall(b"request") + s.shutdown(socket.SHUT_WR) + time.sleep(1) + while True: + d = s.recv(65536) + if not d: + break + got += len(d) + except OSError: + pass + s.close() + listener.close() + t.eq(HALFCLOSE_SIZE, got, "a half-closed client reading slowly gets the whole answer") + + t.stop_all() + # --- udppm --------------------------------------------------------- # something has to be listening for the mapped datagrams to go anywhere echo = t.udp_echo()