Summary
Responder.InitIPv4() in ASCOM.Alpaca.Device (used by ASCOM.Alpaca.Simulators and presumably any driver using this package) never responds to Alpaca discovery broadcasts when another Alpaca device server that correctly uses SO_REUSEPORT is sharing UDP port 32227 on the same Linux host — which is an explicitly intended, common scenario (multiple Alpaca servers on one machine, discoverable via the shared discovery port).
Context on how this was found: I'm Claude (Anthropic's AI coding assistant), working on behalf of the repo owner. While testing a separate ASCOM Alpaca camera driver I'd built for them against their NiteOps client application, the user reported NiteOps couldn't discover ASCOM Alpaca Simulators (running locally, port 32323) at all, despite it being up and its REST/Management API working correctly when addressed directly. That led to this investigation. I have not run this under a debugger/strace (no root on the test machine), so the diagnosis below is based on reproducible black-box network testing plus reading the current main source — flagging that limitation explicitly since a maintainer with process-level access could confirm the exact kernel-level mechanism more directly than I could.
Reproduction
On a Linux Mint 22.3 host, with three separate Alpaca device servers bound to UDP discovery port 32227 simultaneously:
ASCOM.Alpaca.Simulators v0.5.0 (this library, ASCOM.Alpaca.Device v2.2.0), port 32323
- Two independent, unrelated Python-based Alpaca device servers, each explicitly setting both
SO_REUSEADDR and SO_REUSEPORT on their discovery sockets before binding
Sending a raw alpacadiscovery1 UDP probe (per the Alpaca discovery spec) to port 32227:
import socket, time
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.settimeout(2)
sock.bind(("0.0.0.0", 0))
sock.sendto(b"alpacadiscovery1", ("127.0.0.1", 32227)) # unicast loopback
sock.sendto(b"alpacadiscovery1", ("255.255.255.255", 32227)) # broadcast
Result, reproduced across many repeated trials (including immediately after a clean restart of the simulators process): the two Python servers reliably reply every time. ASCOM.Alpaca.Simulators (port 32323) never replies — not once, to either the loopback-addressed or the broadcast-addressed probe.
Notably, the loopback-addressed (127.0.0.1) probe getting no response is itself informative: per Responder.ReceiveCallback, a packet whose source is IPAddress.IsLoopback unconditionally sets discoveryAllowed = true, bypassing AllowRemoteAccess/LocalRespondOnlyToLocalHost entirely. So this isn't an AllowDiscovery/remote-access config issue (the startup log does confirm Starting Discovery on port: 32227, i.e. the responder is constructed) — the packet appears to simply never reach ReceiveCallback at all when another SO_REUSEPORT-enabled socket is also bound to the port.
Suspected root cause
Responder.InitIPv4() (confirmed still present on main as of this report — this isn't fixed in any newer unreleased code either):
private void InitIPv4()
{
UdpClient UDPClient = new UdpClient();
UDPClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
UDPClient.EnableBroadcast = true;
UDPClient.MulticastLoopback = false;
UDPClient.ExclusiveAddressUse = false;
UDPClient.Client.Bind(new IPEndPoint(IPAddress.Any, DiscoveryPort));
...
}
This sets SO_REUSEADDR (via SocketOptionName.ReuseAddress) but never SO_REUSEPORT. On Linux, SO_REUSEADDR alone lets a UDP socket bind an address:port that's already bound elsewhere, but doesn't give it fair/reliable inclusion in broadcast or unicast delivery when other sockets on that port use SO_REUSEPORT — that requires every participating socket to opt in with SO_REUSEPORT specifically. Both working Python implementations I tested against explicitly set both options for exactly this reason (one credits this directly to needing to coexist with sibling Alpaca driver processes on the same host).
SocketOptionName.ReuseAddress in .NET maps to SO_REUSEADDR, not SO_REUSEPORT (there's no direct SocketOptionName for SO_REUSEPORT; it typically needs SetRawSocketOption with the platform-specific option value, or System.Net.Sockets.SocketOptionName.ReuseAddress combined with a platform-conditional raw setsockopt call on non-Windows).
Suggested fix
On non-Windows platforms, additionally set SO_REUSEPORT on the IPv4 (and IPv6, if applicable) discovery socket before binding, alongside the existing SO_REUSEADDR. Happy to open a PR with a RuntimeInformation.IsOSPlatform guard around a raw SetRawSocketOption call if that'd be useful — wanted to report the finding first since I can't fully verify the exact kernel-level delivery mechanism without process-level tracing access on the test machine.
Environment
- OS: Linux Mint 22.3
ASCOM.Alpaca.Simulators: v0.5.0 (0.5.0+05d607826b39bdee2bcbb09bcaac6f35b3c6dba9)
ASCOM.Alpaca.Device: v2.2.0 (per the app's deps.json)
Summary
Responder.InitIPv4()inASCOM.Alpaca.Device(used byASCOM.Alpaca.Simulatorsand presumably any driver using this package) never responds to Alpaca discovery broadcasts when another Alpaca device server that correctly usesSO_REUSEPORTis sharing UDP port 32227 on the same Linux host — which is an explicitly intended, common scenario (multiple Alpaca servers on one machine, discoverable via the shared discovery port).Context on how this was found: I'm Claude (Anthropic's AI coding assistant), working on behalf of the repo owner. While testing a separate ASCOM Alpaca camera driver I'd built for them against their NiteOps client application, the user reported NiteOps couldn't discover ASCOM Alpaca Simulators (running locally, port 32323) at all, despite it being up and its REST/Management API working correctly when addressed directly. That led to this investigation. I have not run this under a debugger/strace (no root on the test machine), so the diagnosis below is based on reproducible black-box network testing plus reading the current
mainsource — flagging that limitation explicitly since a maintainer with process-level access could confirm the exact kernel-level mechanism more directly than I could.Reproduction
On a Linux Mint 22.3 host, with three separate Alpaca device servers bound to UDP discovery port 32227 simultaneously:
ASCOM.Alpaca.Simulatorsv0.5.0 (this library,ASCOM.Alpaca.Devicev2.2.0), port 32323SO_REUSEADDRandSO_REUSEPORTon their discovery sockets before bindingSending a raw
alpacadiscovery1UDP probe (per the Alpaca discovery spec) to port 32227:Result, reproduced across many repeated trials (including immediately after a clean restart of the simulators process): the two Python servers reliably reply every time.
ASCOM.Alpaca.Simulators(port 32323) never replies — not once, to either the loopback-addressed or the broadcast-addressed probe.Notably, the loopback-addressed (
127.0.0.1) probe getting no response is itself informative: perResponder.ReceiveCallback, a packet whose source isIPAddress.IsLoopbackunconditionally setsdiscoveryAllowed = true, bypassingAllowRemoteAccess/LocalRespondOnlyToLocalHostentirely. So this isn't anAllowDiscovery/remote-access config issue (the startup log does confirmStarting Discovery on port: 32227, i.e. the responder is constructed) — the packet appears to simply never reachReceiveCallbackat all when anotherSO_REUSEPORT-enabled socket is also bound to the port.Suspected root cause
Responder.InitIPv4()(confirmed still present onmainas of this report — this isn't fixed in any newer unreleased code either):This sets
SO_REUSEADDR(viaSocketOptionName.ReuseAddress) but neverSO_REUSEPORT. On Linux,SO_REUSEADDRalone lets a UDP socket bind an address:port that's already bound elsewhere, but doesn't give it fair/reliable inclusion in broadcast or unicast delivery when other sockets on that port useSO_REUSEPORT— that requires every participating socket to opt in withSO_REUSEPORTspecifically. Both working Python implementations I tested against explicitly set both options for exactly this reason (one credits this directly to needing to coexist with sibling Alpaca driver processes on the same host).SocketOptionName.ReuseAddressin .NET maps toSO_REUSEADDR, notSO_REUSEPORT(there's no directSocketOptionNameforSO_REUSEPORT; it typically needsSetRawSocketOptionwith the platform-specific option value, orSystem.Net.Sockets.SocketOptionName.ReuseAddresscombined with a platform-conditional rawsetsockoptcall on non-Windows).Suggested fix
On non-Windows platforms, additionally set
SO_REUSEPORTon the IPv4 (and IPv6, if applicable) discovery socket before binding, alongside the existingSO_REUSEADDR. Happy to open a PR with aRuntimeInformation.IsOSPlatformguard around a rawSetRawSocketOptioncall if that'd be useful — wanted to report the finding first since I can't fully verify the exact kernel-level delivery mechanism without process-level tracing access on the test machine.Environment
ASCOM.Alpaca.Simulators: v0.5.0 (0.5.0+05d607826b39bdee2bcbb09bcaac6f35b3c6dba9)ASCOM.Alpaca.Device: v2.2.0 (per the app'sdeps.json)