Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to enable loop back on external interfaces #6

Merged
merged 1 commit into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions multicast_expert/rx_socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class McastRxSocket:
Class to wrap a socket that receives from one or more multicast groups.
"""

def __init__(self, addr_family: int, mcast_ips: List[str], port: int, iface_ip: Optional[str] = None, iface_ips: Optional[List[str]] = None, source_ips: Optional[List[str]] = None, timeout: Optional[float] = None, blocking: Optional[bool] = None):
def __init__(self, addr_family: int, mcast_ips: List[str], port: int, iface_ip: Optional[str] = None, iface_ips: Optional[List[str]] = None, source_ips: Optional[List[str]] = None, timeout: Optional[float] = None, blocking: Optional[bool] = None, enable_external_loopback: bool = False):
"""
Create a socket which receives UDP datagrams over multicast. The socket must be opened
(e.g. using a with statement) before it can be used.
Expand All @@ -43,6 +43,9 @@ def __init__(self, addr_family: int, mcast_ips: List[str], port: int, iface_ip:
this value is processed.
:param blocking: Legacy alias for timeout. If set to True, timeout is set to None (block forever). If set to
False, timeout is set to 0 (nonblocking).
:param enable_external_loopback: Enable loopback of multicast packets sent on external interfaces. If and only
if this option is set to True, McastRxSockets will be able to receive packets sent by McastTxSockets open on
the same address, port, and interface.
"""
self.addr_family = addr_family
self.mcast_ips = mcast_ips
Expand Down Expand Up @@ -100,6 +103,8 @@ def __init__(self, addr_family: int, mcast_ips: List[str], port: int, iface_ip:
self.is_source_specific = not (source_ips is None or len(source_ips) == 0)
if self.is_source_specific and self.addr_family == socket.AF_INET6:
raise MulticastExpertError("Source-specific multicast currently cannot be used with IPv6!")

self.enable_external_loopback = enable_external_loopback

def __enter__(self) -> McastRxSocket:
if self.is_opened:
Expand All @@ -124,9 +129,10 @@ def __enter__(self) -> McastRxSocket:
os_multicast.add_memberships(new_socket, self.mcast_ips, self.iface_infos[iface_ip], self.addr_family)

# On Windows, by default, sent packets are looped back to local sockets on the same interface, even for interfaces
# that are not loopback. Change this by disabling IP_MULTICAST_LOOP unless the loopback interface is used.
# that are not loopback.Change this by disabling IP_MULTICAST_LOOP unless the loopback interface is used or
# if enable_external_loopback is set.
# Note: multicast_expert submitted a PR to clarify this in the Windows docs, and it was accepted!
loop_enabled = (iface_ip == LOCALHOST_IPV4 or iface_ip == LOCALHOST_IPV6)
loop_enabled = self.enable_external_loopback or iface_ip == LOCALHOST_IPV4 or iface_ip == LOCALHOST_IPV6
if self.addr_family == socket.AF_INET:
new_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, loop_enabled)
else:
Expand Down
15 changes: 10 additions & 5 deletions multicast_expert/tx_socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class McastTxSocket:
Class to wrap a socket that sends to one or more multicast groups.
"""

def __init__(self, addr_family: int, mcast_ips: List[str], iface_ip: Optional[str] = None, ttl: int = 1):
def __init__(self, addr_family: int, mcast_ips: List[str], iface_ip: Optional[str] = None, ttl: int = 1, enable_external_loopback: bool = False):
"""
Create a socket which transmits UDP datagrams over multicast. The socket must be opened
(e.g. using a with statement) before it can be used.
Expand All @@ -30,6 +30,9 @@ def __init__(self, addr_family: int, mcast_ips: List[str], iface_ip: Optional[st
:param ttl: Time-to-live parameter to set on the packets sent by this socket, AKA hop limit for ipv6.
This controls the number of routers that the packets may pass through until they are dropped. The
default value of 1 prevents the multicast packets from passing through any routers.
:param enable_external_loopback: Enable receiving multicast packets sent over external interfaces. If and only
if this option is set to True, McastRxSockets will be able to receive packets sent by McastTxSockets open on
the same address, port, and interface.
"""

self.addr_family = addr_family
Expand All @@ -56,6 +59,8 @@ def __init__(self, addr_family: int, mcast_ips: List[str], iface_ip: Optional[st
for mcast_ip in self.mcast_ips:
validate_mcast_ip(mcast_ip, self.addr_family)

self.enable_external_loopback = enable_external_loopback

def __enter__(self) -> McastTxSocket:
if self.is_opened:
raise MulticastExpertError("Attempt to open an McastTxSocket that is already open!")
Expand All @@ -78,13 +83,13 @@ def __enter__(self) -> McastTxSocket:
else: # IPv6
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, self.ttl)

# On Unix, we need to disable multicast loop here. Otherwise, sent packets will get looped back to local
# On Unix, we need to disable multicast loop if we do not want sent packets to get looped back to local
# sockets on the same interface.
if not is_windows:

# On Mac, we do want to keep loopback enabled but only on the loopback interface.
# On Linux, always disable it.
enable_loopback = is_mac and (self.iface_ip == LOCALHOST_IPV4 or self.iface_ip == LOCALHOST_IPV6)
# Enable loopback if enable_external_loopback is set or if using a loopback interface on Mac.
# Otherwise, disable loopback.
enable_loopback = self.enable_external_loopback or (is_mac and (self.iface_ip == LOCALHOST_IPV4 or self.iface_ip == LOCALHOST_IPV6))

if self.addr_family == socket.AF_INET:
self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, enable_loopback)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "multicast_expert"
version = "1.3.0"
version = "1.4.0"
description = "A library to take the fiddly parts out of multicast networking!"
authors = ["Jamie Smith <[email protected]>"]
license = "MIT"
Expand Down
82 changes: 81 additions & 1 deletion tests/test_multicast_expert.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,4 +340,84 @@ def test_v6_loopback_multiple() -> None:
print("\nRx: " + repr(packet_alt))
assert packet_alt is not None
assert packet_alt[0] == test_string_alternate
assert packet_alt[1][0:2] == (multicast_expert.LOCALHOST_IPV6, mcast_tx_sock_alt.getsockname()[1])
assert packet_alt[1][0:2] == (multicast_expert.LOCALHOST_IPV6, mcast_tx_sock_alt.getsockname()[1])


def test_external_loopback_v4() -> None:
"""
Check that packets sent over external interface can be received when `enable_external_loopback` is set.
relativityspace-jsmith marked this conversation as resolved.
Show resolved Hide resolved
"""
with multicast_expert.McastTxSocket(socket.AF_INET,
mcast_ips=[mcast_address_v4],
enable_external_loopback=True) as tx_socket:
assert tx_socket.iface_ip is not None and tx_socket.iface_ip != multicast_expert.LOCALHOST_IPV4

with multicast_expert.McastRxSocket(socket.AF_INET,
mcast_ips=[mcast_address_v4],
iface_ips=[tx_socket.iface_ip],
port=port,
timeout=1,
enable_external_loopback=True) as rx_socket:
tx_socket.sendto(test_string, (mcast_address_v4, port))
data = rx_socket.recv()
assert data == test_string


def test_external_loopback_v6(nonloopback_iface_ipv6: str) -> None:
"""
Check that packets sent over external interface can be received when `enable_external_loopback` is set.
"""
with multicast_expert.McastTxSocket(socket.AF_INET6,
mcast_ips=[mcast_address_v6],
iface_ip=nonloopback_iface_ipv6,
enable_external_loopback=True) as tx_socket:
with multicast_expert.McastRxSocket(socket.AF_INET6,
mcast_ips=[mcast_address_v6],
iface_ips=[nonloopback_iface_ipv6],
port=port,
timeout=1,
enable_external_loopback=True) as rx_socket:
tx_socket.sendto(test_string, (mcast_address_v6, port))
data = rx_socket.recv()
assert data == test_string


def test_external_loopback_disabled_v4() -> None:
"""
Check that packets sent over external interface are not received when `enable_external_loopback` is set to False.
"""
with multicast_expert.McastTxSocket(socket.AF_INET,
mcast_ips=[mcast_address_v4],
enable_external_loopback=False) as tx_socket:
assert tx_socket.iface_ip is not None and tx_socket.iface_ip != multicast_expert.LOCALHOST_IPV4

with multicast_expert.McastRxSocket(socket.AF_INET,
mcast_ips=[mcast_address_v4],
iface_ips=[tx_socket.iface_ip],
port=port,
timeout=1,
enable_external_loopback=False) as rx_socket:
tx_socket.sendto(test_string, (mcast_address_v4, port))
rx_socket.settimeout(0.1)
data = rx_socket.recv()
assert data == None


def test_external_loopback_disabled_v6(nonloopback_iface_ipv6: str) -> None:
"""
Check that packets sent over external interface are not received when `enable_external_loopback` is set to False.
"""
with multicast_expert.McastTxSocket(socket.AF_INET6,
mcast_ips=[mcast_address_v6],
iface_ip=nonloopback_iface_ipv6,
enable_external_loopback=False) as tx_socket:
with multicast_expert.McastRxSocket(socket.AF_INET6,
mcast_ips=[mcast_address_v6],
iface_ips=[nonloopback_iface_ipv6],
port=port,
timeout=1,
enable_external_loopback=False) as rx_socket:
rx_socket.settimeout(0.1)
tx_socket.sendto(test_string, (mcast_address_v6, port))
data = rx_socket.recv()
assert data == None