From 6956916301699466128171e40a10cc4e972e2684 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Dec 2018 17:34:42 +0100 Subject: [PATCH 1/5] add create_server_sock() and has_dual_stack() to socket module (+ tests) --- Lib/socket.py | 69 +++++++++++++++++++++++++++++++++++++++++ Lib/test/test_socket.py | 60 ++++++++++++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 1 deletion(-) diff --git a/Lib/socket.py b/Lib/socket.py index 772b9e185bf1c6e..9d63f5066abd9c0 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -728,6 +728,75 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, else: raise error("getaddrinfo returns an empty list") +def has_dual_stack(): + """Return True if kernel allows creating a socket which is able to + listen for both AF_INET and AF_INET6 connections. + """ + if not has_ipv6 \ + or not hasattr(_socket, 'AF_INET6') \ + or not hasattr(_socket, 'IPV6_V6ONLY'): + return False + try: + with socket(AF_INET6, SOCK_STREAM) as sock: + if not sock.getsockopt(IPPROTO_IPV6, IPV6_V6ONLY): + return True + else: + sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, False) + return True + except error: + return False + +def create_server_sock(address, + reuse_addr=os.name == 'posix' and sys.platform != 'cygwin', + queue_size=5, dual_stack=has_dual_stack()): + """Utility function which creates a TCP server bound to a + (host, port) *address* tuple and return a socket instance. + Internally it takes care of choosing the right address family + depending on the host specified in *address*. + If *host* is an empty string or None all netwrok interfaces are + assumed. + If dual stack is supported by kernel the socket will be able to + listen for both AF_INET and AF_INET6 connections. + + >>> server = create_server_sock((None, 8000)) + >>> while True: + ... sock, addr = server.accept() + ... # handle new sock connection + """ + AF_INET6_ = getattr(_socket, "AF_INET6", 0) + host, port = address + if host == "": + # http://mail.python.org/pipermail/python-ideas/2013-March/019937.html + host = None + # If dual stack is supported and no host was specified '::' will + # accept AF_INET and AF_INET6 connections and all interfaces. + if host is None and dual_stack: + host = "::" + err = None + info = getaddrinfo(host, port, AF_UNSPEC, SOCK_STREAM, 0, AI_PASSIVE) + for res in info: + af, socktype, proto, canonname, sa = res + sock = None + try: + sock = socket(af, socktype, proto) + if reuse_addr: + sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) + if af == AF_INET6_ and dual_stack: + if sock.getsockopt(IPPROTO_IPV6, IPV6_V6ONLY): + sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, False) + sock.bind(sa) + sock.listen(queue_size) + return sock + except error as _: + err = _ + if sock is not None: + sock.close() + + if err is not None: + raise err + else: + raise error("getaddrinfo() returned an empty list") + def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): """Resolve host and port into list of address info entries. diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 626a07797358260..78002e14e75a583 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6009,9 +6009,67 @@ def test_new_tcp_flags(self): self.assertEqual([], unknown, "New TCP flags were discovered. See bpo-32394 for more information") +class CreateServerSockTest(unittest.TestCase): + + def echo_server(self, sock, port=None): + def run(): + with sock: + conn, _ = sock.accept() + with conn: + msg = conn.recv(1024) + if not msg: + return + conn.sendall(msg) + + sock.settimeout(2) + t = threading.Thread(target=run) + t.start() + time.sleep(.1) + + def test_base(self): + port = support.find_unused_port() + with socket.create_server_sock((None, port)) as sock: + self.assertEqual(sock.getsockname()[1], port) + self.assertEqual(sock.type, socket.SOCK_STREAM) + if socket.has_dual_stack(): + self.assertEqual(sock.family, socket.AF_INET6) + else: + self.assertEqual(sock.family, socket.AF_INET) + self.echo_server(sock) + with socket.create_connection(('localhost', port), timeout=2) as cl: + cl.sendall(b'foo') + self.assertEqual(cl.recv(1024), b'foo') + + def test_dual_stack(self): + with socket.create_server_sock((None, 0)) as sock: + self.echo_server(sock) + port = sock.getsockname()[1] + with socket.create_connection(("127.0.0.1", port), timeout=2) as cl: + cl.sendall(b'foo') + self.assertEqual(cl.recv(1024), b'foo') + + with socket.create_server_sock((None, 0)) as sock: + self.echo_server(sock) + port = sock.getsockname()[1] + if socket.has_dual_stack(): + with socket.create_connection(("::1", port), timeout=2) as cl: + cl.sendall(b'foo') + self.assertEqual(cl.recv(1024), b'foo') + else: + self.assertRaises(ConnectionRefusedError, + socket.create_connection, ("::1", port)) + # just stop server + with socket.create_connection(("127.0.0.1", port), timeout=2) \ + as cl: + cl.sendall(b'foo') + cl.recv(1024) + unittest.skip('dual stack cannot be tested as not supported') + + def test_main(): tests = [GeneralModuleTests, BasicTCPTest, TCPCloserTest, TCPTimeoutTest, - TestExceptions, BufferIOTest, BasicTCPTest2, BasicUDPTest, UDPTimeoutTest ] + TestExceptions, BufferIOTest, BasicTCPTest2, BasicUDPTest, + UDPTimeoutTest, CreateServerSockTest] tests.extend([ NonBlockingTCPTests, From 37d867b8ab2ddad1c39cb56196233e8fe7fba6c1 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Dec 2018 20:03:55 +0100 Subject: [PATCH 2/5] write doc --- Doc/library/socket.rst | 20 ++++++++++++++++++++ Doc/whatsnew/3.8.rst | 10 ++++++++++ Lib/socket.py | 33 +++++++++++++++++---------------- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index d466884d6135168..7a3e880e3114dd5 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -595,6 +595,26 @@ The following functions all create :ref:`socket objects `. .. versionchanged:: 3.2 *source_address* was added. +.. function:: create_server_sock(address, *, dual_stack=True, queue_size=5, reuse_addr=True) + + Convenience function which creates a TCP server bound to *address* (a + 2-tuple ``(host, port)``) and return a socket object. If *host* is an empty + string or ``None`` all network interfaces are used. + If dual-stack is supported by kernel and *dual_stack* parameter + is ``True`` the socket will be able to serve both :data:`AF_INET` and + :data:`AF_INET6` connections. If not the right address family will be chosen + based on the host specified in *address*. + *queue_size* is the argument passed to :meth:`socket.listen`. + If *reuse_addr* is ``True`` :data:`socket.SO_REUSEADDR` socket flag is set. + + .. versionadded:: 3.8 + +.. function:: has_dual_stack() + + Return ``True`` if the system allows creating a socket which is able to + listen for both :data:`AF_INET` and :data:`AF_INET6` connections. + + .. versionadded:: 3.8 .. function:: fromfd(fd, family, type, proto=0) diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index 0d0d1ca70e6cd25..64bddddd47fe8b1 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -196,6 +196,16 @@ pathlib contain characters unrepresentable at the OS level. (Contributed by Serhiy Storchaka in :issue:`33721`.) +socket +------ + +Added :meth:`~socket.create_server_sock()`, a convenience function which +creates a TCP server socket able to serve both IPv4 and IPv6 connections. +(Contributed by Giampaolo Rodola in :issue:`17561`.) + +Added :meth:`~socket.has_dual_stack()` function. +(Contributed by Giampaolo Rodola in :issue:`17561`.) + ssl --- diff --git a/Lib/socket.py b/Lib/socket.py index 9d63f5066abd9c0..ffff7d5e4373d05 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -746,22 +746,23 @@ def has_dual_stack(): except error: return False -def create_server_sock(address, - reuse_addr=os.name == 'posix' and sys.platform != 'cygwin', - queue_size=5, dual_stack=has_dual_stack()): - """Utility function which creates a TCP server bound to a - (host, port) *address* tuple and return a socket instance. - Internally it takes care of choosing the right address family - depending on the host specified in *address*. - If *host* is an empty string or None all netwrok interfaces are - assumed. - If dual stack is supported by kernel the socket will be able to - listen for both AF_INET and AF_INET6 connections. - - >>> server = create_server_sock((None, 8000)) - >>> while True: - ... sock, addr = server.accept() - ... # handle new sock connection +def create_server_sock(address, *, dual_stack=has_dual_stack(), queue_size=5, + reuse_addr=os.name == 'posix' and sys.platform != 'cygwin'): + """Convenience function which creates a TCP server bound to + *address* (a 2-tuple (host, port)) and return a socket object. + + If *host* is an empty string or None all network interfaces are assumed. + If dual-stack is supported by kernel and *dual_stack* parameter + is True the socket will be able to serve both AF_INET and AF_INET6 + connections. If not the right address family will be chosen + based on the host specified in *address*. + *queue_size* is the argument passed to socket.listen() method, + *reuse_addr* dictates whether to set SO_REUSEADDR flag. + + >>> with create_server_sock((None, 8000)) as server: + ... while True: + ... sock, addr = server.accept() + ... # handle new sock connection """ AF_INET6_ = getattr(_socket, "AF_INET6", 0) host, port = address From 89dfd700a4ce59785b7b14bb05ec7f9718b03809 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Dec 2018 20:07:43 +0100 Subject: [PATCH 3/5] add mist/news entry --- .../next/Library/2018-12-18-20-06-38.bpo-17561.RbIk_v.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2018-12-18-20-06-38.bpo-17561.RbIk_v.rst diff --git a/Misc/NEWS.d/next/Library/2018-12-18-20-06-38.bpo-17561.RbIk_v.rst b/Misc/NEWS.d/next/Library/2018-12-18-20-06-38.bpo-17561.RbIk_v.rst new file mode 100644 index 000000000000000..7eea5c892219b95 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-12-18-20-06-38.bpo-17561.RbIk_v.rst @@ -0,0 +1,4 @@ +Add socket.create_server_sock() function returning a TCP socket able to serve +both IPv4 and IPv6 connections. Also add socket.has_dual_stack() function, +which tells whether IPv4/6 dual-stack capabilities are supported by the +kernel. (patch by Giampaolo Rodola) From 7ecfca6074dc908c3aa1b2179b1d471eba97f726 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Dec 2018 20:28:30 +0100 Subject: [PATCH 4/5] reword doc --- Doc/library/socket.rst | 8 ++++---- .../next/Library/2018-12-18-20-06-38.bpo-17561.RbIk_v.rst | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 7a3e880e3114dd5..9117056eaa24781 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -602,8 +602,8 @@ The following functions all create :ref:`socket objects `. string or ``None`` all network interfaces are used. If dual-stack is supported by kernel and *dual_stack* parameter is ``True`` the socket will be able to serve both :data:`AF_INET` and - :data:`AF_INET6` connections. If not the right address family will be chosen - based on the host specified in *address*. + :data:`AF_INET6` (IPv4 / IPv6) connections. If not the right address family + will be chosen based on the host specified in *address*. *queue_size* is the argument passed to :meth:`socket.listen`. If *reuse_addr* is ``True`` :data:`socket.SO_REUSEADDR` socket flag is set. @@ -611,8 +611,8 @@ The following functions all create :ref:`socket objects `. .. function:: has_dual_stack() - Return ``True`` if the system allows creating a socket which is able to - listen for both :data:`AF_INET` and :data:`AF_INET6` connections. + Return ``True`` if the kernel supports creating a socket which can handle + both :data:`AF_INET` and :data:`AF_INET6` (IPv4 / IPv6) connections. .. versionadded:: 3.8 diff --git a/Misc/NEWS.d/next/Library/2018-12-18-20-06-38.bpo-17561.RbIk_v.rst b/Misc/NEWS.d/next/Library/2018-12-18-20-06-38.bpo-17561.RbIk_v.rst index 7eea5c892219b95..c5d362d7288253a 100644 --- a/Misc/NEWS.d/next/Library/2018-12-18-20-06-38.bpo-17561.RbIk_v.rst +++ b/Misc/NEWS.d/next/Library/2018-12-18-20-06-38.bpo-17561.RbIk_v.rst @@ -1,4 +1,4 @@ Add socket.create_server_sock() function returning a TCP socket able to serve both IPv4 and IPv6 connections. Also add socket.has_dual_stack() function, -which tells whether IPv4/6 dual-stack capabilities are supported by the -kernel. (patch by Giampaolo Rodola) +which tells whether the kernel supports creating a socket which can handle +both IPv4 a IPv6 connections. (patch by Giampaolo Rodola) From 5d3c5cccd43e9a79cd3e8b145cf1df6afbe5d772 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 18 Dec 2018 20:33:09 +0100 Subject: [PATCH 5/5] update docstring --- Lib/socket.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/socket.py b/Lib/socket.py index ffff7d5e4373d05..b571c4e526b8004 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -729,8 +729,8 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, raise error("getaddrinfo returns an empty list") def has_dual_stack(): - """Return True if kernel allows creating a socket which is able to - listen for both AF_INET and AF_INET6 connections. + """Return True if the kernel supports creating a socket which + can handle both AF_INET and AF_INET6 (IPv4 / IPv6) connections. """ if not has_ipv6 \ or not hasattr(_socket, 'AF_INET6') \