Skip to content

Add rudimentary support for broadcast via aiosync-client #292

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
6 changes: 5 additions & 1 deletion aiocoap/cli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def build_parser():
p.add_argument('--payload-initial-szx', help="Size exponent to limit the initial block's size (0 ≙ 16 Byte, 6 ≙ 1024 Byte)", metavar="SZX", type=int)
p.add_argument('--content-format', help="Content format of the --payload data. If a known format is given and --payload has a non-file argument, conversion is attempted (currently only JSON/Python-literals to CBOR).", metavar="MIME")
p.add_argument('--no-set-hostname', help="Suppress transmission of Uri-Host even if the host name is not an IP literal", dest="set_hostname", action='store_false', default=True)
p.add_argument('-b', '--broadcast', help="Set SO_BROADCAST for UDP non-interative/single requests", dest="broadcast", action='store_true', default=False)
p.add_argument('-v', '--verbose', help="Increase the debug output", action="count")
p.add_argument('-q', '--quiet', help="Decrease the debug output", action="count")
p.add_argument('--interactive', help="Enter interactive mode", action="store_true") # careful: picked before parsing
Expand Down Expand Up @@ -335,7 +336,10 @@ async def single_request(args, context):
async def single_request_with_context(args):
"""Wrapper around single_request until sync_main gets made fully async, and
async context managers are used to manage contexts."""
context = await aiocoap.Context.create_client_context()
parser = build_parser()
options = parser.parse_args(args)

context = await aiocoap.Context.create_client_context(broadcast=options.broadcast)
try:
await single_request(args, context)
finally:
Expand Down
6 changes: 2 additions & 4 deletions aiocoap/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ async def _append_tokenmanaged_transport(self, token_interface_constructor):
self.request_interfaces.append(tman)

@classmethod
async def create_client_context(cls, *, loggername="coap", loop=None, transports: Optional[List[str]] = None):
async def create_client_context(cls, *, loggername="coap", loop=None, transports: Optional[List[str]] = None, broadcast: bool = False):
"""Create a context bound to all addresses on a random listening port.

This is the easiest way to get a context suitable for sending client
Expand All @@ -159,7 +159,7 @@ async def create_client_context(cls, *, loggername="coap", loop=None, transports
if transportname == 'udp6':
from .transports.udp6 import MessageInterfaceUDP6
await self._append_tokenmanaged_messagemanaged_transport(
lambda mman: MessageInterfaceUDP6.create_client_transport_endpoint(mman, log=self.log, loop=loop))
lambda mman: MessageInterfaceUDP6.create_client_transport_endpoint(mman, log=self.log, loop=loop, broadcast=broadcast))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any good reason to make this a flag? IOW: Will anything bad come of aiocoap always setting that option?

We're setting a lot of socket options even though we don't know yet whether we'll need them on this particular occasion. (For example, V6ONLY=0 is set even though we may not use IPv4 at all).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good question. I think the downside is that superuser privileges (or possibly some CAP_* on Linux) are required to set this particular ioctl, but I need to check on that

Copy link
Author

@mzpqnxow mzpqnxow May 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm misremembering regarding the requirement of additional privileges, the EPERM was coming from an earlier version of this patch which also added SO_BINDTODEVICE

You may wonder why on earth that would be necessary- the server software I was dealing with actually required the universal broadcast address 255.255.255.255 for a specific endpoint, and I'm working in a multi-homed environment with multiple interfaces having +BROADCAST

In the end I decided it would be outside the scope of this PR to add an option to force the interface, especially without any discussion. My final conclusion was to not attempt to solve that problem with changes to aiocoap; there are a few workarounds good enough for that exceptional case

tl; dr; your suggestion on defaulting to SO_BROADCAST seems to be a good one as far as I can tell

elif transportname == 'simple6':
from .transports.simple6 import MessageInterfaceSimple6
await self._append_tokenmanaged_messagemanaged_transport(
Expand Down Expand Up @@ -241,13 +241,11 @@ async def create_server_context(cls, site, bind=None, *, loggername="coap-server
lambda mman: MessageInterfaceSimple6.create_client_transport_endpoint(mman, log=self.log, loop=loop))
elif transportname == 'tinydtls':
from .transports.tinydtls import MessageInterfaceTinyDTLS

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't change whitespace or other unrelated code in PRs. If this is your editor's doing, you may want to reign it in on code bases that don't follow that editor's particular coding style.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, sorry 'bout that, this one (or two) slipped through. I managed to avoid my urge to run black against the entire codebase and submit that as a PR 😜

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The concern is duly noted, and as soon as I have the number of large branches down a bit, and have verified that no code is becoming less readable, black will be the one color in which you get aiocoap, and CI will mark any specks of color with a big red cross mark.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The concern is duly noted, and as soon as I have the number of large branches down a bit, and have verified that no code is becoming less readable, black will be the one color in which you get aiocoap, and CI will mark any specks of color with a big red cross mark.

The world wants to know- when will we have PEP-484 across the codebase and no red marks next to mypy? 😉

On a serious note though, I'd be happy to help with that effort- for the public interfaces or the entire project- if you're interested obviously, and if time permits

Personally I now find it nearly impossible to write good interfaces without it, though that may be evidence of my lack of discipline rather than evidence of the broad value of type annotations

await self._append_tokenmanaged_messagemanaged_transport(
lambda mman: MessageInterfaceTinyDTLS.create_client_transport_endpoint(mman, log=self.log, loop=loop))
# FIXME end duplication
elif transportname == 'tinydtls_server':
from .transports.tinydtls_server import MessageInterfaceTinyDTLSServer

await self._append_tokenmanaged_messagemanaged_transport(
lambda mman: MessageInterfaceTinyDTLSServer.create_server(bind, mman, log=self.log, loop=loop, server_credentials=self.server_credentials))
elif transportname == 'simplesocketserver':
Expand Down
8 changes: 7 additions & 1 deletion aiocoap/tokenmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,13 @@ def request(self, request):
self.outgoing_requests[key] = request
request.on_interest_end(functools.partial(self.outgoing_requests.pop, key, None))

'''
'''Not implemented
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above on coding style.

def multicast_request(self, request):
return MulticastRequest(self, request).responses
'''

'''Not implemented
def broadcast_request(self, request):
return MulticastRequest(self, request).responses
'''

19 changes: 17 additions & 2 deletions aiocoap/transports/udp6.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,20 @@ def _local_port(self):
return self.transport.get_extra_info('socket').getsockname()[1]

@classmethod
async def _create_transport_endpoint(cls, sock, ctx: interfaces.MessageManager, log, loop, multicast=[]):
async def _create_transport_endpoint(cls, sock, ctx: interfaces.MessageManager, log, loop, multicast=[], broadcast: bool = False):
try:
sock.setsockopt(socket.IPPROTO_IPV6, socknumbers.IPV6_RECVPKTINFO, 1)
except NameError:
raise RuntimeError("RFC3542 PKTINFO flags are unavailable, unable to create a udp6 transport.")
if broadcast is True:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above: Can this be unconditional?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤞

try:
# This shouldn't be needed, at least not when using --broadcast
# with aiocoap-client.
if not sock.getsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking and then setting when not yet set is no faster or better than just setting it.

sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
except Exception:
log.fatal("Unable to set socket to SO_BROADCAST; setsockopt() failed")
raise
Comment on lines +262 to +263
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
log.fatal("Unable to set socket to SO_BROADCAST; setsockopt() failed")
raise
log.warning("Unable to set socket to SO_BROADCAST; continuing, but requests can not be sent to broadcast addresses.")

When there is no extra flag, this can best-effort.

if socknumbers.HAS_RECVERR:
sock.setsockopt(socket.IPPROTO_IPV6, socknumbers.IPV6_RECVERR, 1)
# i'm curious why this is required; didn't IPV6_V6ONLY=0 already make
Expand Down Expand Up @@ -299,9 +308,15 @@ async def _create_transport_endpoint(cls, sock, ctx: interfaces.MessageManager,
return protocol

@classmethod
async def create_client_transport_endpoint(cls, ctx: interfaces.MessageManager, log, loop):
async def create_client_transport_endpoint(cls, ctx: interfaces.MessageManager, log, loop, broadcast: bool = False):
sock = socket.socket(family=socket.AF_INET6, type=socket.SOCK_DGRAM)
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
if broadcast is True:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is redundant with the code in _create_transport_endpoint, which is called right after this.

try:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
except Exception:
log.fatal("Unable to set socket to SO_BROADCAST; setsockopt() failed")
raise

return await cls._create_transport_endpoint(sock, ctx, log, loop)

Expand Down