Amiga.org
Amiga computer related discussion => Amiga Software Issues and Discussion => Topic started by: clark on November 13, 2023, 04:22:56 PM
-
Hi all,
As I've been getting back into Amiga, I've been using Envoy a lot in my network. It's really useful!
Does anyone know who owns the rights to Envoy and if they'd be willing to either sell them or release it to the public? It's a great piece of software, but it needs some updating and TLC.
I tried to e-mail Heinz Wrobel who made the last update in 2004, but I never got a response. Not even sure if the e-mail is still valid or if he's even still alive.
Of course, you can't buy it anymore, and the currently available pirated/cracked copies have a broken installation script. I've taken a stab at writing a new one with a little script that combines the pirated ADFs + the last Envoy 3.1 update into a working installation bundle. That's available here: https://github.com/xlark/EnvoyInstall. If anyone is interested.
I'd love to do more, but without the source code and distribution rights, I'm kind of hamstrung.
Thanks,
-Clark
-
Heinz Wrobel still owns it. Multiple people have asked him over the years if he would sell it or release it and he always says no. Just another stubborn butthurt bastard like all the others in Amiga land. Shame, as Envoy was a lovely piece of software for the time.
-
It’s old and hackish anyways, why not create something better from scratch? Start with concepts of what you wish to accomplish (is printer sharing still imprtant? is not using tcp/ip still a thing?) and then draft some protocols for how to achieve what you want, and then try to implement them, one after the other.
-
What about NetFS (https://aminet.net/package/comm/net/NetFS-revised)?
I have plans to write my own rl=https://keasigmadelta.com/zitafs-aos]network drive system[/url]. Sadly, I've had to put it aside for now.
Hans
-
I love Envoy. Even if when it was new it was still a bit behind the curve of AppleTalk and Windows for Workgroups. But it's of that era - a generation where people still used proprietary transport layer protocols instead of only TCP and UDP. At the same time it was still IP based and therefore could share layer 3 with other stacks without clashing with each other. Nothing hackish about it at all.
I loved how well designed it was for the Amiga architecture. Services weren't daemons in the unix sense, instead, like all things amiga, they were shared libraries. There are same great examples of services that extend it's functionality on aminet, like the conf and talk services (source code available too).
It's correct to say that today in 2024 we would likely just use TCP/IP based services, and similarly that no one needs a LAN printer server - chances are any modern printer is its own server or accepts direct wifi print jobs, while cloud drives replaced peer to peer file sharing.
It's a shame Heinz is such a prick about making it available one way or another.
-
It’s old and hackish anyways.
It is old, but it is not that terrible. The important part which Envoy is built upon (nipc.library) has not aged gracefully, but Envoy 2.x and 3.1 have improved it. Its original limitations did not go away, though. Just like a TCP/IP stack, Envoy assumes it has complete ownership of the configured NICs and it will not play nice with others ("my way or the highway"). I think that this could be resolved by implementing an nipc.library which delegates all the UDP traffic wrangling to bsdsocket.library (it could happen). The "small catch" being that the modern Envoy protocols are wholly undocumented. Envoy 1.6 (from 1994) may not be interoperable with the more recent versions.
I still think the Envoy architecture is a pretty sweet design, but I do wonder how well the "reliable datagram protocol" of 1992 fares today. There's no documentation on its origins, let alone if there's any relation to the other n+1 curiously named "reliable datagram protocol" versions of that era. Remembering how quickly Envoy could freeze in a 1993 10BASE2 LAN, I reckon there's still work to be done to research how resilient the protocol really is. The members of the Amiga networking group were either reassigned or dismissed before Envoy could have matured.
Customary gratuitous rant which can be safely ignored: Envoy is not a file sharing or printer sharing solution. These services merely build upon the underlying reliable UDP protocol which nipc.library handles. Envoy is a platform which provides limited authentication and security (in 1992 everything was so simple), auto-configuring peer-to-peer networking without a central authority and enable data exchange between the clients. You could build distributed computing applications using that platform.
-
At the same time it was still IP based and therefore could share layer 3 with other stacks without clashing with each other. Nothing hackish about it at all.
Its original limitations did not go away, though. Just like a TCP/IP stack, Envoy assumes it has complete ownership of the configured NICs and it will not play nice with others ("my way or the highway").
So, which is it? "share layer 3 with other stacks without clashing" or "not play nice with others"?
One quite profound "clash" that I recall, is that of what UID 0 is.
-
What about NetFS (https://aminet.net/package/comm/net/NetFS-revised)?
NetFS Revised is a lovely piece of software for Amiga compatible filesystem and ARexx port sharing.
Here's a recent video how it works:
https://www.youtube.com/watch?v=SirKvJ03EnM
-
NetFS Revised is a lovely piece of software for Amiga compatible filesystem and ARexx port sharing.
I have an issue with client systems resetting/crashing when remote file share server reboots.
(But it seems to only happen when Workbench is loaded?)
Many years ago, I briefly started implementing the netfs protocol in python, with the goal of having a lighter alternative to smb and nfs for sharing file system to Amiga from non-amiga systems... but that project got lost in time.
-
So, which is it? "share layer 3 with other stacks without clashing" or "not play nice with others"?
One quite profound "clash" that I recall, is that of what UID 0 is.
The Envoy version for which I saw the source code talks directly to the SANA-II device driver. This is a problem because Envoy assumes that every IP or ARP frame which is transmitted is intended for its use. If a TCP/IP stack on the same machine uses the same SANA-II device driver, Envoy and the TCP/IP stack will grab the next available frame, being unable to check first if it was intended for them.
While you could make use of the SANA-II S2_PacketFilter option to figure out if the source or destination port number is 376 in the IP frame, the original Envoy did not support this option (this was added with Envoy 2.0, if memory serves). The bigger problem are the ARP frames. ARP replies will get read by Envoy or the TCP/IP stack, whoever gets lucky first. The S2_PacketFilter option is no help here either, since the SANA-II device has to offer each frame to every client until one says "it's mine". The filter works well for unicast, but for ARP replies which are broadcast there could be unexpected behaviour, such as the SANA-II driver not implementing the S2_PacketFilter option correctly.
Hiking the Envoy UDP traffic to how the TCP/IP stack does this would solve this overlap in responsibilities. Right now I don't see much of an alternative.
-
So are we approaching “hackish” yet?
-
@kolla
You seem to be unable to differentiate between versions 1, 2 and 3. Olsen is talking about version 1. Perhaps you have reading comprehension issues. I would see a doctor if I were you.
-
So are we approaching “hackish” yet?
As I wrote, there is no wiggle room for changes to be made in how Envoy deals with SANA-II devices shared by another IP protocol stack (could be the Oxxi ACS client, could be AmiTCP) when broadcast ARP replies enter the picture. Whoever queued the first S2_READ command will deprive the other stack from learning about the ARP reply's message.
In my opinion, a TCP/IP stack as the means to handle ARP and UDP for Envoy would be in the best position to solve this problem because we still have more than one such stack which is still being maintained and developed. It could also deal well with SANA-II devices which do not implement the S2_PacketFilter option, or which implement it incorrectly.
Because the ownership of the Envoy code as it was in 1992-1994 and beyond is complex, I'm just blowing soap bubbles here ;) Could be that there will come a time when Envoy might make a comeback and one hypothetical way to solve the IP and ARP frame sharing issue might come in handy.
-
@kolla
You seem to be unable to differentiate between versions 1, 2 and 3. Olsen is talking about version 1. Perhaps you have reading comprehension issues. I would see a doctor if I were you.
As far as I know, Envoy versions 2 and 3 still cannot share the same SANA-II device with AmiTCP, INet-225, Miami, Roadshow, etc. unless you change the frame types for IP and ARP from 2048 and 2054, respectively, to something else.
While versions 2 and 3 are improvements over their precursors, it should not be necessary to tinker with the frame types. There is little harm in this, though, e.g. 2049 and 2055 are presently tagged as "unavailable" or "private" (see https://standards-oui.ieee.org/ethertype/eth.txt).
I would like to see what Envoy does (or attempts to do) implemented robustly. We have nothing comparable to it today, as far as I can tell. What we have is no longer being sold, maintained or developed and there is no replacement for it.
-
Perhaps you have reading comprehension issues.
Or maybe you have such issues? As olsen points out, there are "clashes" that needs to be resorted manually for Envoy and TCP/IP stacks to work well on same SANA-II device. This was still needed last time I tried latest version of Envoy (which I admit is like many years ago now). This is what I meant with "hackish".
Here i from the 3.1 update on aminet (http://aminet.net/package/comm/net/Envoy3_1Update)
If you are using a TCP/IP protocol stack like AS225, INet-225, Miami, or AmiTCP, you need to have SANA-II networking devices that support multiple protocol stacks. Most devices do. You also should not use the same IP or ARP numbers for Envoy that you are using for TCP/IP. Unless you are specifying your own numbers during the configuration, this Installation will automatically increase the common defaults to avoid conflicts.
(It is also worth nothing that this update has binary patch for a2065.device and ariadne_ii.device - hm, why is that)
-
@olsen
On a slightly related note, with OS 3.2...
- what do C:Owner and C:Group actually do?
- what do C:List LFORMAT %U and %G actually show?
What user/group database is supposed to be in place here? Where is this supposed to go?
Integration with Roadshow usergroup.library? Old MuFS? Does Amiga need equivalent of nsswitch? :D
-
@olsen
On a slightly related note, with OS 3.2...
- what do C:Owner and C:Group actually do?
They set up which specific user and which members of a specific group may access the files, directories, etc. on a volume you control and which is made available for use through a networked file system that enforces these access controls. One example of this kind of file system being the one implemented by the Envoy filesystem.service, which in turn draws upon the local Envoy user and group database via accounts.library.
Basically, "Owner" and "Group" work much like "Protect" does, except that they change the FileInfoBlock.fib_User and FileInfoBlock.fib_Group information of the directories, files, etc. in question.
- what do C:List LFORMAT %U and %G actually show?
They show the name of the user and group, respectively, who/which may have access rights for the respective file, directory, etc.
It's the file system's business to translate the numeric IDs stored in the FileInfoBlock.fib_User and FileInfoBlock.fib_Group into human-readable information. If this is a networked file system, such as exported by Envoy's filesystem.service, then the mapping between the numeric IDs and their associated names is again performed through accounts.library.
If the file system cannot translate the numeric IDs for user and/or group, then the "List" command will just show the numbers as a fall-back.
What user/group database is supposed to be in place here? Where is this supposed to go?
If this is Envoy's filesystem.service providing access, you would first populate its database through the "Accounts Manager" tool, then assign the access rights for the local volume which you want to export.
Integration with Roadshow usergroup.library? Old MuFS? Does Amiga need equivalent of nsswitch? :D
The way this was designed, it's the network file system which needs to enforce the access rights. Because the client has to be granted permission to access the exported files, directories, etc. the file system server can effectively deny access.
So much for theory. The hashing algorithm behind the Envoy user/group database is very weak and could be cracked easily. Which is probably no surprise, given how little was known about one-way hash functions back in the day outside the circle of security experts. Deploying a Unix-like password hash function scheme based on DES in 1992 would have been a real firecracker. At about that time, there were two BSD 4.4Lite-2 source code variants, the domestic and the export version. Only the weakened export version was deemed "safe". So, no fancy password hash function for Envoy ;)
I have no idea how effective MuFS actually was/is. The Amiga operating system is single user by default because the kind of controls a multi-user operating system provides would have been hard to implement without memory protection. Costly, too, since it would have had to be built around dedicated hardware (such as the Sun 2/3 made use of) making the entire system run more slowly (how does that fit into the feature set of a game machine?). Retrofitting security onto an operating system not designed to support it is always painful :(
-
Here i from the 3.1 update on aminet (http://aminet.net/package/comm/net/Envoy3_1Update)
(It is also worth nothing that this update has binary patch for a2065.device and ariadne_ii.device - hm, why is that)
Because there were bugs in each which both could and should be fixed? ;) The a2065.device is not exactly notorious for being full of surprises, but it should be. In the Roadshow bsdsocket.library I had to work around a2065.device bugs which were easily triggered. Even the original AmiTCP 3.0b2 code featured a workaround which passed the respective I/O request unit pointer in register A3 when calling AbortIO(). And unless S2_DEVICEQUERY features Sana2DeviceQuery.SizeAvailable = offsetof(struct Sana2DeviceQuery, RawMTU), some a2065.device versions (there are at least three which I know of) will crash instantly.
-
@olsen
Thank you for taking time to answer this, it’s a bit puzzling with these commands in OS 3.2 that don’t appear to do anything, it’s not obvious they are only for network filesystems, in practice only Envoy. And back in the days, also MuFS (which integrated better with IP stacks, as uid/gid 0 is “root/admin” and not “nobody/guest”). Do you know of any other filesystem supporting this? Your own smbfs perhaps? I don’t see any attempt to open any library or resource when using owner/group/list…
As for bugs in sana2 devices, yes… that’s always a problem, not all sana2 devices were “prepared” to be shared.
MuFS was fairly effective when using standard commands/software, but without memory protection it’s all kinda moot. Geert jumped off Amiga when CBM folded, and has since been active Linux developer, where he to this day is the man signing off patches to Linux/68k (as well as powerpc iirc). He still has his A4000 I think :)
-
@olsen
Thank you for taking time to answer this, it’s a bit puzzling with these commands in OS 3.2 that don’t appear to do anything, it’s not obvious they are only for network filesystems, in practice only Envoy.
Indeed, the expected file system behaviour is specific to Envoy's "EnvoyFileSystem" (mounts the shared network volumes) and its counterpart "filesystem.service" (enables a directory or volume to be shared and mounted).
And back in the days, also MuFS (which integrated better with IP stacks, as uid/gid 0 is “root/admin” and not “nobody/guest”).
With Envoy's "filesystem.service", both the user ID and the group ID value 0 refer to the Administrator. By default, this will deny access to all the directories, files, etc. exported by the volume. You have to deliberately add the users and groups who/which should be able to access the contents of the volume and then make use of the Owner and Group shell commands to grant them access to the specific directories and files which should be available. Which is a sound approach, I would say. This is not a "toy", it has a sensible technical foundation.
Do you know of any other filesystem supporting this? Your own smbfs perhaps? I don’t see any attempt to open any library or resource when using owner/group/list…
I am not that well-read when it comes to file systems and neworked file systems in particular ;) From what I gather, the approach taken by filesystem.service and EnvoyFileSystem emerges directly from the concepts which underpin Envoy: simplicity and usability vs. requiring the user to set up every little thing correctly and getting slapped in the face if just one of these things is not entirely correct. SMBv1/CIFS features administrative level management commands which are sent by the client to the server, which are comparable to the filesystem.service/EnvoyFileSystem message set (changing/querying authorization rights, ACLs, etc.), but on a far larger scale. They also work only within the SMB "ecosystem", e.g. Windows NT and beyond (the official CIFS documentation does not go into any kind of detail for these messages). My smbfs port is definitely a client which relies upon the server to grant it access to the resources it is entitled to. It has no business even trying to provide administrative access methods.
As for bugs in sana2 devices, yes… that’s always a problem, not all sana2 devices were “prepared” to be shared.
Not all SANA-II devices were doing the boring parts of the job to begin with. Namely, validating the OpenDevice() parameters, the I/O requests submitted to Begin() and AbortIO(), and dealing gracefully with wonky input, returning a helpful error code. The whole point of using exec devices is in being able to validate and reject invalid input in real time. If you cannot even accomplish that, why even bother? :(
-
here's a relevant follow up I'd love to hear an answer from @olsen about
Both Inet225 and Envoy offer service discovery and control mechanisms for 'daemons' effectively. Also, RexxMast could be considered to be a 'daemon' in the unix sense. While AddDataTypes could be considered to be a kind of registry for plugins.
Modern platforms obviously have systemctl, launchd etc for this kind of thing. What would a system wide service discovery and management solution for amiga OS look like?
-
I'd say "commodity interface", aka CX.
All MUI software by default offer a commodity interface, and it's darn handy.
-
I didn't ask you, and once again, you totally misunderstood the question.
-
I didn't ask you, and once again, you totally misunderstood the question.
Damn, Kola was actually trying to help and you shut him down like that.
-
here's a relevant follow up I'd love to hear an answer from @olsen about
Both Inet225 and Envoy offer service discovery and control mechanisms for 'daemons' effectively. Also, RexxMast could be considered to be a 'daemon' in the unix sense. While AddDataTypes could be considered to be a kind of registry for plugins.
Modern platforms obviously have systemctl, launchd etc for this kind of thing. What would a system wide service discovery and management solution for amiga OS look like?
Hm... I have worked with INet-225 when it became part of the Amiga-Surfer product for Amiga Technologies GmbH, but I did not explore the technical aspects which distinguished it, compared to AmiTCP (at the time the only contender). INet-225 did support inetd-style daemon services, but so did AmiTCP. It was baked into the BSD TCP/IP "stack". Envoy made use of local UDP broadcast, for peers to announce their presence. Not exactly what would be called service discovery these days, which would be Zero configuration networking (Zeroconf) which makes use of multicast DNS.
RexxMast certainly qualifies as a daemon in the same vein as the inetd-style daemon services offered by TCP/IP stacks. Except that this is a local service which does not support network operations or discovery. It could have been interesting to make an Envoy service that would have allowed for remote ARexx script execution. Envoy's networking model was designed as an extension of the exec FindPort/PutMsg/WaitPort/ReplyMsg message passing.
The DataTypes system is something else, in my opinion. Its focus is on representing on-disk data via BOOPSI objects, with datatypes.library finding the relevant disk-loaded classes to match the file contents. You may ask for DataTypes to produce an object for you, but you may not get it.
We don't really have a general service discovery mechanism in the Amiga operating system...
-
> We don't really have a general service discovery mechanism in the Amiga operating system...
Yes exactly I was just wondering if it was something you'd ever thought of how it might work. Obviously we can Run ><NIL: headless things that open message ports and sockets and the like, and yes as kolla says commodities can give you something similar for GUI based applets that have ephemeral user interfaces but benefit from persistent sessions. But perhaps a hypothetical future Amiga OS would benefit from a centralised service management layer. AFAIK you didn't implement inetd in Roadshow?
what's the use case? here's one - every time you want to use an external rexxport, you need to check if the port exists, and if it doesn't, run the host for that port, if you even know where to find it. Or you want to open on a named PUBSCREEN. But you're first to arrive, and the screen isn't open yet.
-
As I wrote, there is no wiggle room for changes to be made in how Envoy deals with SANA-II devices shared by another IP protocol stack (could be the Oxxi ACS client, could be AmiTCP) when broadcast ARP replies enter the picture. Whoever queued the first S2_READ command will deprive the other stack from learning about the ARP reply's message.
According to the SANA-II standard, stacks should be able to share received frames:
Another recommendation for the ``magic cookie`` is to use it to maintain a separate packet read queue for each device opener. This would allow multiple protocol stacks that all wish to receive the same packet type to work together without having to "know" about each other as Envoy and AS225 do right now. What does multiple protocol stack support mean? Basically this means that each opener gets all the packets necessary. If a packet comes in that fills a request for more than one opener of the device, all of them will get a copy of the packet. This feature should never be left out of a device design. If it is missing, the usefulness of the device is severely limited.
The SLIP and A2065 driver now do this, so it would be possible (for example) to run Envoy, AS225 and the AmiTCP package together on the same hardware without conflicts.
(https://wiki.amigaos.net/wiki/Revision_2_%2B_3#Buffer_Management)
I know this is only a recommendation, but it seems it should allow Envoy and a TCP stack to coexist, at least when using certain drivers, such as the A2065 one mentioned above and several of my own.
-
According to the SANA-II standard, stacks should be able to share received frames:
(https://wiki.amigaos.net/wiki/Revision_2_%2B_3#Buffer_Management)
Indeed, but in practice Envoy steals every single frame it receives before a TCP/IP stack or other consumer (Oxxi Novell Netware client, etc.) could claim it. I used to suspect that this was a limitation of the respective SANA-II drivers I saw this behaviour with, but it is a feature of the filter to say to the driver that the frame was intended for it, thank you very much, even if this turned out not to be the case. This likely was a bug in the Envoy implementation and is, as far as I know, still present in Envoy 3.x. The Envoy filter should never ever consume broadcast ARP frames and it should never consume IP frames with unicast UDP traffic unless intended for port 376. Yet it does :(
I know this is only a recommendation, but it seems it should allow Envoy and a TCP stack to coexist, at least when using certain drivers, such as the A2065 one mentioned above and several of my own.
My theory is that the code was not tested as thoroughly as it should have been and the handling of the SANA-II filter by Envoy slipped by undetected. It may have been tested only within an isolated network which was used by the Amiga Networking Group.
-
But I don't see anything in the SANA-II docs to say that a driver shouldn't also offer a frame to other stacks if one stack's filter hook accepts the frame. So to me it seems more like a bug in the driver if Envoy interferes with another stack in this way, as it might be a valid case for multiple stacks to be interested in the same frame, and my interpretation of the docs is that a TRUE result from a stack's filter hook just means "yes, I'll take a copy of that", not "hands off, it's mine" :)
It might be interesting to do a test with 3c589.device, Envoy and Roadshow to see if they get on well together, as I think they should.
-
commodities can give you something similar for GUI based applets
Commodities don't necessarily have GUI, what makes you think so?
AutoPoint, ClickToFront and NoCapsLock for example, don 't.
what's the use case? here's one - every time you want to use an external rexxport, you need to check if the port exists, and if it doesn't, run the host for that port, if you even know where to find it.
Not sure what you mean with "external rexxport", but if you mean an arexx port of a program, and your complaint is that you need to run the program to make the port available... yes, is that hard? What I hear is that you want the system itself to detect that something is trying to open an arbitrarily named arexx port, and then launch the software that's somehow is registered with this arbitrary port name... correct? The question becomes, how to detect what port is attempted, and how to ensure the right software is started so that something can answer on the port correctly before the client gives up. In principle you can have a script that listens to all the ports you have set up in a "servers" like file, and when something connects, it will shut down its port and launch the software so it can open the real port... however, the client may have become unhappy at this point, because of the port "hand over", or simply because launching the software may take too long.
Or you want to open on a named PUBSCREEN. But you're first to arrive, and the screen isn't open yet.
Well, there's a (badom-tish) commodity for that - MUI:PSI, it can do just this, and I am sure there are other pub screen handlers that do the same. Should the OS come with a pubscreen commodity handler built in? In my view - Absolutely! But certain OS developers didn't think it's worth wasting time on, as there are already third party tools for this.
-
But I don't see anything in the SANA-II docs to say that a driver shouldn't also offer a frame to other stacks if one stack's filter hook accepts the frame. So to me it seems more like a bug in the driver if Envoy interferes with another stack in this way, as it might be a valid case for multiple stacks to be interested in the same frame, and my interpretation of the docs is that a TRUE result from a stack's filter hook just means "yes, I'll take a copy of that", not "hands off, it's mine" :)
Except that the documentation does not go into detail how you should implement the filter on the driver's side. There exists exactly one officlal Commodore implementation of the "S2_PacketFilter" feature in the internal "slip.device", which features the version string "slip 38.1 (17.2.94)".
Correction, half an hour later: I reread the implementation's code and realized that I was looking at the wrong thing.
So, let's start over again how "slip.device" deals with the "S2_PacketFilter" feature.
When a new frame arrives, "slip.device" will offer a preview of the frame to every client which currently has a CMD_READ pending. If the client's hook code likes what it sees and returns a non-zero value, then the CMD_READ request's CopyToBuffer() callback will transfer a copy of the frame to the client and the CMD_READ command will be "terminated" by removing it from the queue, calling ReplyMsg() if the IOF_QUICK flag is not set. If the hook code returns zero, then the client will not receive a copy of the frame. "slip.device" walks through the list of clients which submitted CMD_READ commands until it has reached the end.
This means that every client gets asked if it wants to process the frame or not. Nobody can steal a frame. Every client receives a copy if it wants to.
If this is how it should work, it raises the question if every SANA-II driver out there that supports the "S2_PacketFilter" feature implements it correctly.
The Envoy 3.0 filter hook might just be a "victim" of SANA-II drivers not implementing the filter feature consistently.
-
For Envoy 3.0 the packet filter has the effect of stealing multicast/broadcast traffic from everybody. Reading the documentation and the implementation for the S2_PacketFilter hook feature, it is framed as an optimization, which it would have been if the client wouldn't have become the sole owner of the frame. Saying "no, it's not for me" to a frame is helpful, but saying "this looks interesting" ends up removing the frame has unexpected consequences. I wonder what purpose this behaviour could possibly have that makes sense?
Speed? It was created in another age with slow networks and slow computers, compared to today's world.
Best regards,
Niels
-
The Envoy 3.0 filter hook might just be a "victim" of SANA-II drivers not implementing the filter feature consistently.
I'd put money on it ;D However, cnet.device seems to do it properly (to take another random example that has source code available).
-
I'd put money on it ;D However, cnet.device seems to do it properly (to take another random example that has source code available).
I checked and both ariadne.device and ariadne_ii.device do the right thing, too :) Wish I could have said that for ppp-serial.device and ppp-ethernet.device, but I had last updated these in 2017 (time flies) and did not know any better at the time. "Luckily", ppp-serial.device and ppp-ethernet.device are typically "single client only".