Skip to content

Commit 22ff6b5

Browse files
authored
Break apart new_socket to be testable (#867)
1 parent dcf18c8 commit 22ff6b5

2 files changed

Lines changed: 70 additions & 19 deletions

File tree

tests/utils/test_net.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import errno
99
import ifaddr
1010
import pytest
11+
import socket
1112
import unittest
1213

1314
from zeroconf._utils import net as netutils
@@ -99,3 +100,42 @@ def test_autodetect_ip_version():
99100
assert r.autodetect_ip_version([]) is r.IPVersion.V4Only
100101
assert r.autodetect_ip_version(["::1", "1.2.3.4"]) is r.IPVersion.All
101102
assert r.autodetect_ip_version(["::1"]) is r.IPVersion.V6Only
103+
104+
105+
def test_disable_ipv6_only_or_raise():
106+
"""Test that IPV6_V6ONLY failing logs a nice error message and still raises."""
107+
errors_logged = []
108+
109+
def _log_error(*args):
110+
nonlocal errors_logged
111+
errors_logged.append(args)
112+
113+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
114+
with pytest.raises(OSError), patch.object(netutils.log, "error", _log_error), patch(
115+
"socket.socket.setsockopt", side_effect=OSError
116+
):
117+
netutils.disable_ipv6_only_or_raise(sock)
118+
119+
assert (
120+
errors_logged[0][0]
121+
== 'Support for dual V4-V6 sockets is not present, use IPVersion.V4 or IPVersion.V6'
122+
)
123+
124+
125+
@pytest.mark.skipif(not hasattr(socket, 'SO_REUSEPORT'), reason="System does not have SO_REUSEPORT")
126+
def test_set_so_reuseport_if_available_is_present():
127+
"""Test that setting socket.SO_REUSEPORT only OSError errno.ENOPROTOOPT is trapped."""
128+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
129+
with pytest.raises(OSError), patch("socket.socket.setsockopt", side_effect=OSError):
130+
netutils.set_so_reuseport_if_available(sock)
131+
132+
with patch("socket.socket.setsockopt", side_effect=OSError(errno.ENOPROTOOPT, None)):
133+
netutils.set_so_reuseport_if_available(sock)
134+
135+
136+
@pytest.mark.skipif(hasattr(socket, 'SO_REUSEPORT'), reason="System has SO_REUSEPORT")
137+
def test_set_so_reuseport_if_available_not_present():
138+
"""Test that we do not try to set SO_REUSEPORT if it is not present."""
139+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
140+
with patch("socket.socket.setsockopt", side_effect=OSError):
141+
netutils.set_so_reuseport_if_available(sock)

zeroconf/_utils/net.py

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,34 @@ def normalize_interface_choice(
161161
return result
162162

163163

164+
def disable_ipv6_only_or_raise(s: socket.socket) -> None:
165+
"""Make V6 sockets work for both V4 and V6 (required for Windows)."""
166+
try:
167+
s.setsockopt(_IPPROTO_IPV6, socket.IPV6_V6ONLY, False)
168+
except OSError:
169+
log.error('Support for dual V4-V6 sockets is not present, use IPVersion.V4 or IPVersion.V6')
170+
raise
171+
172+
173+
def set_so_reuseport_if_available(s: socket.socket) -> None:
174+
"""Set SO_REUSEADDR on a socket if available."""
175+
# SO_REUSEADDR should be equivalent to SO_REUSEPORT for
176+
# multicast UDP sockets (p 731, "TCP/IP Illustrated,
177+
# Volume 2"), but some BSD-derived systems require
178+
# SO_REUSEPORT to be specified explicitly. Also, not all
179+
# versions of Python have SO_REUSEPORT available.
180+
# Catch OSError and socket.error for kernel versions <3.9 because lacking
181+
# SO_REUSEPORT support.
182+
if not hasattr(socket, 'SO_REUSEPORT'):
183+
return
184+
185+
try:
186+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) # pylint: disable=no-member
187+
except OSError as err:
188+
if err.errno != errno.ENOPROTOOPT:
189+
raise
190+
191+
164192
def new_socket( # pylint: disable=too-many-branches
165193
bind_addr: Union[Tuple[str], Tuple[str, int, int]],
166194
port: int = _MDNS_PORT,
@@ -180,28 +208,11 @@ def new_socket( # pylint: disable=too-many-branches
180208
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
181209

182210
if ip_version == IPVersion.All:
183-
# make V6 sockets work for both V4 and V6 (required for Windows)
184-
try:
185-
s.setsockopt(_IPPROTO_IPV6, socket.IPV6_V6ONLY, False)
186-
except OSError:
187-
log.error('Support for dual V4-V6 sockets is not present, use IPVersion.V4 or IPVersion.V6')
188-
raise
211+
disable_ipv6_only_or_raise(s)
189212

190213
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
191214

192-
# SO_REUSEADDR should be equivalent to SO_REUSEPORT for
193-
# multicast UDP sockets (p 731, "TCP/IP Illustrated,
194-
# Volume 2"), but some BSD-derived systems require
195-
# SO_REUSEPORT to be specified explicitly. Also, not all
196-
# versions of Python have SO_REUSEPORT available.
197-
# Catch OSError and socket.error for kernel versions <3.9 because lacking
198-
# SO_REUSEPORT support.
199-
if hasattr(socket, 'SO_REUSEPORT'):
200-
try:
201-
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) # pylint: disable=no-member
202-
except OSError as err:
203-
if err.errno != errno.ENOPROTOOPT:
204-
raise
215+
set_so_reuseport_if_available(s)
205216

206217
if port == _MDNS_PORT:
207218
ttl = struct.pack(b'B', 255)

0 commit comments

Comments
 (0)