Skip to content

Commit

Permalink
feat: Add socket-activated systemd service for discovery (#218)
Browse files Browse the repository at this point in the history
Introduce a system-wide daemon listening on /run/wsdd.socket,
which is managed by systemd.
When a client first connects to the socket the daemon is started.
Multiple clients can connect and safely use the API simultaneously.

The service runs in discovery-only mode.

Uses a fixed multicast source-port 37020, which is used by the
socket-activated discovery daemon.

Allow port 37020 in firewall profiles and split configurations for
discovery (client) and host roles.

Co-authored-by: Steffen Christgau <[email protected]>
  • Loading branch information
aleasto and christgau authored Jan 5, 2025
1 parent 2b8ca12 commit 13d2b0f
Show file tree
Hide file tree
Showing 10 changed files with 60 additions and 8 deletions.
7 changes: 7 additions & 0 deletions etc/firewalld/services/wsdd-discovery.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<service>
<short>Web Services Dynamic Discovery client</short>
<description>Discovery of other Web Services Dynamic Discovery hosts, using a fixed UDP source port for the client (wsdd)</description>
<port port="37020" protocol="udp"/>
<destination ipv4="239.255.255.250" ipv6="FF02::C"/>
</service>
4 changes: 2 additions & 2 deletions etc/firewalld/services/wsdd-http.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<service>
<short>Web Services Dynamic Discovery host daemon (HTTP Interface)</short>
<description>wsdd implements a Web Service Discovery host daemon. This enables (Samba) hosts, like your local NAS device, to be found by Web Service Discovery Clients like Windows.</description>
<short>Web Services Dynamic Discovery HTTP Interface</short>
<description>HTTP interface of the Web Service Discovery host daemon, such as wsdd. This service enables the host to be discovered by WSD clients such as Windows.</description>
<port port="5357" protocol="tcp"/>
</service>
1 change: 1 addition & 0 deletions etc/firewalld/services/wsdd.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<short>Web Services Dynamic Discovery host daemon</short>
<description>wsdd implements a Web Service Discovery host daemon. This enables (Samba) hosts, like your local NAS device, to be found by Web Service Discovery Clients like Windows.</description>
<port port="3702" protocol="udp"/>
<port port="37020" protocol="udp"/>
<destination ipv4="239.255.255.250" ipv6="FF02::C"/>
<include service="wsdd-http"/>
</service>
17 changes: 17 additions & 0 deletions etc/systemd/wsdd-discovery.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[Unit]
Description=Web Services Dynamic Discovery service
Documentation=man:wsdd(8)
Requires=wsdd-discovery.socket

[Service]
Type=simple
; Use /etc/default/wsdd for defaults, if it exists
EnvironmentFile=-/etc/default/wsdd
; The service is put into an empty runtime directory chroot,
; i.e. the runtime directory which usually resides under /run
ExecStart=/usr/bin/wsdd --shortlog --chroot=/run/wsdd-discovery --source-port=37020 --no-host --discovery $WSDD_DISCOVERY_PARAMS
DynamicUser=yes
User=wsdd-discovery
Group=wsdd-discovery
RuntimeDirectory=wsdd-discovery
AmbientCapabilities=CAP_SYS_CHROOT
9 changes: 9 additions & 0 deletions etc/systemd/wsdd-discovery.socket
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[Unit]
Description=Web Services Dynamic Discovery API socket
Documentation=man:wsdd(8)

[Socket]
ListenStream=%t/wsdd.socket

[Install]
WantedBy=sockets.target
1 change: 1 addition & 0 deletions etc/systemd/wsdd.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
# Refer to the wsdd(8) man page for details

WSDD_PARAMS=""
WSDD_DISCOVERY_PARAMS=""
4 changes: 4 additions & 0 deletions etc/ufw/applications.d/wsd-client
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[wsd-client]
title=WSD Client
description=Allow discovery of WSD-supporting devices on the local network
ports=37020/udp
4 changes: 4 additions & 0 deletions etc/ufw/applications.d/wsd-host
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[wsdd]
title=WSD Host
description=Make host visible for device discovery
ports=3702/udp|5357/tcp
4 changes: 0 additions & 4 deletions etc/ufw/applications.d/wsdd

This file was deleted.

17 changes: 15 additions & 2 deletions src/wsdd.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
except ModuleNotFoundError:
from xml.etree.ElementTree import fromstring as ETfromString

try:
import systemd.daemon
except ModuleNotFoundError:
# Non-systemd host
pass

WSDD_VERSION: str = '0.8'

Expand Down Expand Up @@ -1133,7 +1138,7 @@ class ApiServer:
address_monitor: 'NetworkAddressMonitor'
clients: List[asyncio.StreamWriter]

def __init__(self, aio_loop: asyncio.AbstractEventLoop, listen_address: bytes,
def __init__(self, aio_loop: asyncio.AbstractEventLoop, listen_address: Any,
address_monitor: 'NetworkAddressMonitor') -> None:
self.server = None
self.clients = []
Expand All @@ -1147,7 +1152,11 @@ async def create_server(self, aio_loop: asyncio.AbstractEventLoop, listen_addres
# It appears mypy is not able to check the argument to create_task and the return value of start_server
# correctly. The docs say start_server returns a coroutine and the create_task takes a coro. And: It works.
# Thus, we ignore type errors here.
if isinstance(listen_address, int) or listen_address.isnumeric():
if isinstance(listen_address, socket.SocketType):
# create socket from systemd file descriptor/socket
self.server = await aio_loop.create_task(asyncio.start_unix_server( # type: ignore
self.on_connect, sock=listen_address))
elif isinstance(listen_address, int) or listen_address.isnumeric():
self.server = await aio_loop.create_task(asyncio.start_server( # type: ignore
self.on_connect, host='localhost', port=int(listen_address), reuse_address=True,
reuse_port=True))
Expand Down Expand Up @@ -2079,6 +2088,10 @@ def main() -> int:
api_server = None
if args.listen:
api_server = ApiServer(aio_loop, args.listen, nm)
elif 'systemd' in sys.modules:
fds = systemd.daemon.listen_fds()
if fds:
api_server = ApiServer(aio_loop, socket.socket(fileno=fds[0]), nm)

# get uid:gid before potential chroot'ing
if args.user is not None:
Expand Down

0 comments on commit 13d2b0f

Please sign in to comment.