SDL_net: api: Added broadcast packets, creation properties, datagram.c example.

From 153c45ff3ab624dad17a8a5687e14ec84d2feacb Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Thu, 14 May 2026 23:54:40 -0400
Subject: [PATCH] api: Added broadcast packets, creation properties, datagram.c
 example.

Fixes #97.
---
 CMakeLists.txt             |   1 +
 examples/datagram.c        | 148 ++++++++++++++++++++++++
 examples/echo-server.c     |   2 +-
 examples/simple-http-get.c |   2 +-
 examples/voipchat.c        |   2 +-
 include/SDL3_net/SDL_net.h |  90 ++++++++++++++-
 src/SDL_net.c              | 223 ++++++++++++++++++++++++++++++++-----
 7 files changed, 436 insertions(+), 32 deletions(-)
 create mode 100644 examples/datagram.c

diff --git a/CMakeLists.txt b/CMakeLists.txt
index ec0c8fa6..7a792560 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -339,6 +339,7 @@ if(SDLNET_SAMPLES)
     add_sdl_net_example_executable(resolve-hostnames examples/resolve-hostnames.c)
     add_sdl_net_example_executable(get-local-addrs examples/get-local-addrs.c)
     add_sdl_net_example_executable(echo-server examples/echo-server.c)
+    add_sdl_net_example_executable(datagram examples/datagram.c)
 
     # Build at least one example in C90
     set_property(TARGET get-local-addrs PROPERTY C_STANDARD 90)
diff --git a/examples/datagram.c b/examples/datagram.c
new file mode 100644
index 00000000..b8993da7
--- /dev/null
+++ b/examples/datagram.c
@@ -0,0 +1,148 @@
+#include <SDL3/SDL_main.h>
+#include <SDL3/SDL.h>
+#include <SDL3_net/SDL_net.h>
+
+static NET_DatagramSocket *sock = NULL;  /* you talk over this, client or server. */
+static NET_Address *server_addr = NULL;  /* address of the server you're talking to, NULL if you _are_ the server. */
+static Uint16 server_port = 9781;
+
+static void print_usage(const char *prog) {
+    SDL_Log("USAGE: %s <hostname|ip|-> [--help] [--server] [--port X] [--simulate-failure Y]", prog);
+}
+
+static void run_datagram(int argc, char **argv)
+{
+    const char *hostname = NULL;
+    bool is_server = false;
+    int simulate_failure = 0;
+    int i;
+    NET_Address *socket_address = NULL;
+    const SDL_PropertiesID props = SDL_CreateProperties();
+    SDL_SetBooleanProperty(props, NET_PROP_DATAGRAM_SOCKET_ALLOW_BROADCAST_BOOLEAN, true);
+
+    for (i = 1; i < argc; i++) {
+        const char *arg = argv[i];
+        if (SDL_strcmp(arg, "--help") == 0) {
+            print_usage(argv[0]);
+            return;
+        } else if (SDL_strcmp(arg, "--server") == 0) {
+            is_server = true;
+        } else if ((SDL_strcmp(arg, "--port") == 0) && (i < (argc-1))) {
+            server_port = (Uint16) SDL_atoi(argv[++i]);
+        } else if ((SDL_strcmp(arg, "--simulate-failure") == 0) && (i < (argc-1))) {
+            simulate_failure = (int) SDL_atoi(argv[++i]);
+        } else {
+            hostname = arg;
+        }
+    }
+
+    simulate_failure = SDL_clamp(simulate_failure, 0, 100);
+    if (simulate_failure) {
+        SDL_Log("Simulating failure at %d percent", simulate_failure);
+    }
+
+    if (!is_server && !hostname) {
+        print_usage(argv[0]);
+        return;
+    }
+
+    if (is_server) {
+        if (hostname) {
+            SDL_Log("SERVER: Resolving binding hostname '%s' ...", hostname);
+            socket_address = NET_ResolveHostname(hostname);
+            if (socket_address) {
+                if (NET_WaitUntilResolved(socket_address, -1) == NET_FAILURE) {
+                    NET_UnrefAddress(socket_address);
+                    socket_address = NULL;
+                }
+            }
+        #if 0
+        } else {
+            int num_addresses;
+            NET_Address **addresses;
+            addresses = NET_GetLocalAddresses(&num_addresses);
+            if (addresses == NULL || num_addresses <= 0) {
+                SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to to get local addresses: %s", SDL_GetError());
+            } else {
+                socket_address = addresses[0];
+                NET_RefAddress(socket_address);
+            }
+        #endif
+        }
+        if (socket_address) {
+            SDL_Log("SERVER: Listening on %s:%d.", NET_GetAddressString(socket_address), server_port);
+        } else {
+            SDL_Log("SERVER: Listening on port %d", server_port);
+        }
+    } else {
+        if (SDL_strcmp(hostname, "-") == 0) {
+            SDL_Log("CLIENT: Broadcasting instead of unicasting to a specific address");
+        } else {
+            SDL_Log("CLIENT: Resolving server hostname '%s' ...", hostname);
+            server_addr = NET_ResolveHostname(hostname);
+            if (server_addr) {
+                if (NET_WaitUntilResolved(server_addr, -1) == NET_FAILURE) {
+                    NET_UnrefAddress(server_addr);
+                    server_addr = NULL;
+                }
+            }
+
+            if (!server_addr) {
+                SDL_Log("CLIENT: Failed! %s", SDL_GetError());
+                SDL_Log("CLIENT: Giving up.");
+                return;
+            }
+        
+            SDL_Log("CLIENT: Server is at %s:%d.", NET_GetAddressString(server_addr), (int) server_port);
+        }
+    }
+
+    /* server _must_ be on the requested port. Clients can take anything available, server will respond to where it sees it come from. */
+    sock = NET_CreateDatagramSocket(socket_address, is_server ? server_port : 0, props);
+    SDL_DestroyProperties(props);
+    NET_UnrefAddress(socket_address);
+    if (!sock) {
+        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create datagram socket: %s", SDL_GetError());
+    } else {
+        if (simulate_failure) {
+            NET_SimulateDatagramPacketLoss(sock, simulate_failure);
+        }
+
+        if (!is_server) {
+            Uint8 buf[128];
+            SDL_zeroa(buf);
+            SDL_Log("CLIENT: %s %d bytes...", server_addr ? "Sending" : "Broadcasting", (int) sizeof (buf));
+            NET_SendDatagram(sock, server_addr, server_port, buf, sizeof (buf));
+        } else {
+            while (NET_WaitUntilInputAvailable((void **) &sock, 1, -1) >= 0) {
+                NET_Datagram *dgram = NULL;
+                if (NET_ReceiveDatagram(sock, &dgram) && (dgram != NULL)) {
+                    SDL_Log("SERVER: got %d-byte datagram from %s:%d", (int) dgram->buflen, NET_GetAddressString(dgram->addr), (int) dgram->port);
+                    NET_DestroyDatagram(dgram);
+                }
+            }
+        }
+    }
+
+    SDL_Log("Shutting down...");
+
+    NET_UnrefAddress(server_addr);
+    server_addr = NULL;
+    NET_DestroyDatagramSocket(sock);
+    sock = NULL;
+}
+
+int main(int argc, char **argv)
+{
+    if (!NET_Init()) {
+        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "NET_Init failed: %s", SDL_GetError());
+        SDL_Quit();
+        return 1;
+    }
+
+    run_datagram(argc, argv);
+
+    NET_Quit();
+    return 0;
+}
+
diff --git a/examples/echo-server.c b/examples/echo-server.c
index ec660f76..91cc9f5b 100644
--- a/examples/echo-server.c
+++ b/examples/echo-server.c
@@ -52,7 +52,7 @@ int main(int argc, char **argv)
         }
     }
 
-    NET_Server *server = NET_CreateServer(server_addr, server_port);
+    NET_Server *server = NET_CreateServer(server_addr, server_port, 0);
     if (!server) {
         SDL_Log("Failed to create server: %s", SDL_GetError());
     } else {
diff --git a/examples/simple-http-get.c b/examples/simple-http-get.c
index 40838712..5c32f444 100644
--- a/examples/simple-http-get.c
+++ b/examples/simple-http-get.c
@@ -27,7 +27,7 @@ int main(int argc, char **argv)
             SDL_Log("%s is %s", argv[i], NET_GetAddressString(addr));
             char *req = NULL;
             SDL_asprintf(&req, "GET / HTTP/1.0\r\nHost: %s\r\n\r\n", argv[i]);
-            NET_StreamSocket *sock = req ? NET_CreateClient(addr, 80) : NULL;
+            NET_StreamSocket *sock = req ? NET_CreateClient(addr, 80, 0) : NULL;
             if (!req) {
                 SDL_Log("Out of memory!");
             } else if (!sock) {
diff --git a/examples/voipchat.c b/examples/voipchat.c
index adfd9468..d3886998 100644
--- a/examples/voipchat.c
+++ b/examples/voipchat.c
@@ -347,7 +347,7 @@ static void run_voipchat(int argc, char **argv)
     }
 
     /* server _must_ be on the requested port. Clients can take anything available, server will respond to where it sees it come from. */
-    sock = NET_CreateDatagramSocket(socket_address, is_server ? server_port : 0);
+    sock = NET_CreateDatagramSocket(socket_address, is_server ? server_port : 0, 0);
     NET_UnrefAddress(socket_address);
     if (!sock) {
         SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create datagram socket: %s", SDL_GetError());
diff --git a/include/SDL3_net/SDL_net.h b/include/SDL3_net/SDL_net.h
index 7aabb737..67ce9288 100644
--- a/include/SDL3_net/SDL_net.h
+++ b/include/SDL3_net/SDL_net.h
@@ -640,8 +640,13 @@ typedef struct NET_StreamSocket NET_StreamSocket;
  * you do not have to byteswap it into "network order," as the library will
  * handle that for you.
  *
+ * There are currently no extra properties for creating a client, so `props`
+ * should be zero. A future revision of SDL_net may add additional (optional)
+ * properties.
+ *
  * \param address the address of the remote server to connect to.
  * \param port the port on the remote server to connect to.
+ * \param props properties of the new client. Specify zero for defaults.
  * \returns a new NET_StreamSocket, pending connection, or NULL on error; call
  *          SDL_GetError() for details.
  *
@@ -653,7 +658,7 @@ typedef struct NET_StreamSocket NET_StreamSocket;
  * \sa NET_GetConnectionStatus
  * \sa NET_DestroyStreamSocket
  */
-extern SDL_DECLSPEC NET_StreamSocket * SDLCALL NET_CreateClient(NET_Address *address, Uint16 port);
+extern SDL_DECLSPEC NET_StreamSocket * SDLCALL NET_CreateClient(NET_Address *address, Uint16 port, SDL_PropertiesID props);
 
 /**
  * Block until a stream socket has connected to a server.
@@ -750,8 +755,24 @@ typedef struct NET_Server NET_Server;
  * you do not have to byteswap it into "network order," as the library will
  * handle that for you.
  *
+ * The caller may supply properties to customize behavior. This is optional,
+ * and a value of zero for `props` will request defaults for all properties.
+ *
+ * These are the supported properties:
+ *
+ * - `NET_PROP_SERVER_REUSEADDR_BOOLEAN`: true if the server should
+ *   be created even if a previous server has recently used this address.
+ *   For various reasons, networks prefer that there be some delay between
+ *   apps reusing the same address, but this can be problematic when
+ *   iterating quickly, for software development purposes or just restarting
+ *   a crashed service. This property defaults to true (although it should be
+ *   noted that, at the operating system level, this defaults to false!). If
+ *   this property is false and the OS feels that not enough time has elapsed,
+ *   server creation will fail and this function will report an error.
+ *
  * \param addr the _local_ address to listen for connections on, or NULL.
  * \param port the port on the local address to listen for connections on.
+ * \param props properties of the new server. Specify zero for defaults.
  * \returns a new NET_Server, or NULL on error; call SDL_GetError() for
  *          details.
  *
@@ -763,7 +784,10 @@ typedef struct NET_Server NET_Server;
  * \sa NET_AcceptClient
  * \sa NET_DestroyServer
  */
-extern SDL_DECLSPEC NET_Server * SDLCALL NET_CreateServer(NET_Address *addr, Uint16 port);
+extern SDL_DECLSPEC NET_Server * SDLCALL NET_CreateServer(NET_Address *addr, Uint16 port, SDL_PropertiesID props);
+
+#define NET_PROP_SERVER_REUSEADDR_BOOLEAN     "NET.server.reuseaddr"
+
 
 /**
  * Create a stream socket for the next pending client connection.
@@ -1213,10 +1237,37 @@ typedef struct NET_Datagram
  * you do not have to byteswap it into "network order," as the library will
  * handle that for you.
  *
+ * The caller may supply properties to customize behavior. This is optional,
+ * and a value of zero for `props` will request defaults for all properties.
+ *
+ * These are the supported properties:
+ *
+ * - `NET_PROP_DATAGRAM_SOCKET_REUSEADDR_BOOLEAN`: true if the socket should
+ *   be created even if a previous socket has recently used this address.
+ *   For various reasons, networks prefer that there be some delay between
+ *   apps reusing the same address, but this can be problematic when
+ *   iterating quickly, for software development purposes or just restarting
+ *   a crashed service. This property defaults to true (although it should be
+ *   noted that, at the operating system level, this defaults to false!). If
+ *   this property is false and the OS feels that not enough time has elapsed,
+ *   socket creation will fail and this function will report an error.
+ * - `NET_PROP_DATAGRAM_SOCKET_ALLOW_BROADCAST_BOOLEAN`: true if the socket
+ *   should allow broadcasting. At the lower level, this will set
+ *   `SO_BROADCAST` for IPv4 sockets, to allow sending to the subnet's
+ *   broadcast address at the OS level. For IPv6, it'll join the all-nodes
+ *   link-local multicast group, ff02::1, allowing sending and receiving
+ *   there, more or less simulating the usual IPv4 broadcast semantics. Other
+ *   protocols take similar approaches. If you do not intend to send or
+ *   receive broadcast packets on this socket, set this property to false, or
+ *   omit it, as it defaults to false. Note: IPv4 will still be able to
+ *   receive broadcast packets without this option, but IPv6 will not. Also
+ *   see notes about sending to a broadcast address in NET_SendDatagram().
+ *
  * \param addr the local address to listen for connections on, or NULL to
  *             listen on all available local addresses.
  * \param port the port on the local address to listen for connections on, or
  *             zero for the system to decide.
+ * \param props properties of the new socket. Specify zero for defaults.
  * \returns a new NET_DatagramSocket, or NULL on error; call SDL_GetError()
  *          for details.
  *
@@ -1227,7 +1278,11 @@ typedef struct NET_Datagram
  * \sa NET_GetLocalAddresses
  * \sa NET_DestroyDatagramSocket
  */
-extern SDL_DECLSPEC NET_DatagramSocket * SDLCALL NET_CreateDatagramSocket(NET_Address *addr, Uint16 port);
+extern SDL_DECLSPEC NET_DatagramSocket * SDLCALL NET_CreateDatagramSocket(NET_Address *addr, Uint16 port, SDL_PropertiesID props);
+
+#define NET_PROP_DATAGRAM_SOCKET_REUSEADDR_BOOLEAN         "NET.datagram_socket.reuseaddr"
+#define NET_PROP_DATAGRAM_SOCKET_ALLOW_BROADCAST_BOOLEAN   "NET.datagram_socket.allow_broadcast"
+
 
 /**
  * Send a new packet over a datagram socket to a remote system.
@@ -1264,8 +1319,35 @@ extern SDL_DECLSPEC NET_DatagramSocket * SDLCALL NET_CreateDatagramSocket(NET_Ad
  * should assume it is no longer usable and should destroy it with
  * SDL_DestroyDatagramSocket().
  *
+ * Sending to a NULL address is treated as a request to broadcast a packet.
+ * Note that this will report failure immediately if the socket was not
+ * created with broadcast permission. Broadcast packets are (more or less)
+ * sent to every machine on the LAN, unconditionally.
+ *
+ * **WARNING**: It is possible to build a game where everyone is playing on
+ * the same LAN, and every player is simply broadcasting packets. This is
+ * absolutely the wrong thing to do, however. Broadcast packets go to every
+ * device on the LAN, whether they want them or not. The game DOOM, in its
+ * heyday, was capable of [bringing entire networks to their knees](https://doomwiki.org/wiki/Doom_in_workplaces),
+ * as many players on the same network would all be broadcasting relentlessly.
+ *
+ * In practice, broadcasting sparingly can be useful for certain
+ * functionality: a LAN-only client broadcasting a few packets to ask for
+ * available servers, and running servers replying directly to that client
+ * without broadcasting at all, is reasonable and safe. Once clients and
+ * servers have found each other, they can communicate directly without
+ * any broadcasting at all. For peer-to-peer games, once connection is
+ * established, it's better to either send unique packets to each known
+ * player, or use a multicasting (which works like broadcast, but only
+ * routes packets to devices that are explicitly listening for it).
+ *
+ * With IPv6, which doesn't support broadcasts, broadcasting is faked with
+ * multicast to the all-nodes link-local multicast group, ff02::1, either on a
+ * specific interface or letting the OS choose the default. Other protocols
+ * might fake broadcast operations in similar ways in the future.
+ *
  * \param sock the datagram socket to send data through.
- * \param address the NET_Address object address.
+ * \param address the NET_Address object address. May be NULL to broadcast.
  * \param port the address port.
  * \param buf a pointer to the data to send as a single packet.
  * \param buflen the size of the data to send, in bytes.
diff --git a/src/SDL_net.c b/src/SDL_net.c
index 35dbb68d..feb66d96 100644
--- a/src/SDL_net.c
+++ b/src/SDL_net.c
@@ -253,6 +253,10 @@ static int num_interfaces = 0;
 static NetworkInterface *interfaces = NULL;
 static SDL_AtomicInt interfaces_have_changed;
 
+// Other stuff...
+static NET_Address *ipv6_broadcast_addr = NULL;
+
+
 // between lo and hi (inclusive; it can return lo or hi itself, too!).
 static int RandomNumberBetween(const int lo, const int hi)
 {
@@ -1026,6 +1030,13 @@ bool NET_Init(void)
         }
     }
 
+    struct sockaddr_in6 sa_in6;
+    SDL_zero(sa_in6);
+    if (inet_pton(AF_INET6, "ff02::1", &sa_in6.sin6_addr) == 1) {
+        sa_in6.sin6_family = AF_INET6;
+        ipv6_broadcast_addr = CreateSDLNetAddrFromSockAddr((const struct sockaddr *) &sa_in6, sizeof (sa_in6));
+    }
+
     return true;  // good to go.
 
 failed:
@@ -1103,6 +1114,9 @@ void NET_Quit(void)
     SDL_assert(!interface_rwlock);
     SDL_assert(num_interfaces == 0);
 
+    NET_UnrefAddress(ipv6_broadcast_addr);
+    ipv6_broadcast_addr = NULL;
+
     #ifdef SDL_PLATFORM_WINDOWS
     WSACleanup();
     #endif
@@ -1367,7 +1381,7 @@ struct NET_StreamSocket
     Uint64 simulated_failure_until;
 };
 
-NET_StreamSocket *NET_CreateClient(NET_Address *addr, Uint16 port)
+NET_StreamSocket *NET_CreateClient(NET_Address *addr, Uint16 port, SDL_PropertiesID props)
 {
     if (addr == NULL) {
         SDL_InvalidParamError("address");
@@ -1466,7 +1480,7 @@ struct NET_Server
     Socket handle_pool[4];
 };
 
-NET_Server *NET_CreateServer(NET_Address *addr, Uint16 port)
+NET_Server *NET_CreateServer(NET_Address *addr, Uint16 port, SDL_PropertiesID props)
 {
     if (addr && (((NET_Status) SDL_GetAtomicInt(&addr->status)) != NET_SUCCESS)) {
         SDL_SetError("Address is not resolved");  // strictly speaking, this should be a local interface, but a resolved address can fail later.
@@ -1510,6 +1524,8 @@ NET_Server *NET_CreateServer(NET_Address *addr, Uint16 port)
         }
     }
 
+    const int reuseaddr = SDL_GetBooleanProperty(props, NET_PROP_SERVER_REUSEADDR_BOOLEAN, true) ? 1 : 0;
+
     // Make sockets for all desired interfaces; if addr!=NULL, this is one socket on one interface,
     //  but if addr==NULL, it might be multiple sockets for IPv4, IPv6, etc, bound to their INADDR_ANY equivalent.
     struct addrinfo *ainfo = addrwithport;
@@ -1533,7 +1549,7 @@ NET_Server *NET_CreateServer(NET_Address *addr, Uint16 port)
             setsockopt(handle, IPPROTO_IPV6, IPV6_V6ONLY, (const char *) &one, sizeof (one));  // if this fails, oh well.
         }
 
-        setsockopt(handle, SOL_SOCKET, SO_REUSEADDR, (const char *) &one, sizeof (one));
+        setsockopt(handle, SOL_SOCKET, SO_REUSEADDR, (const char *) &reuseaddr, sizeof (reuseaddr));
 
         int rc = bind(handle, ainfo->ai_addr, (SockLen) ainfo->ai_addrlen);
         if (rc == SOCKET_ERROR) {
@@ -1850,6 +1866,7 @@ typedef struct NET_DatagramSocketHandle
     Socket handle;
     int family;
     int protocol;
+    NET_Address *broadcast;
 } NET_DatagramSocketHandle;
 
 struct NET_DatagramSocket
@@ -1867,10 +1884,64 @@ struct NET_DatagramSocket
     NET_Datagram **pending_output;
     int pending_output_len;
     int pending_output_allocation;
+    bool allow_broadcast;
 };
 
 
-NET_DatagramSocket *NET_CreateDatagramSocket(NET_Address *addr, Uint16 port)
+// if `addr` isn't from NET_GetLocalAddresses(), lookup the actual NET_Address
+// that matches it. This is so people can get an interface address specified on
+// the command line by "resolving" the command line string into a NET_Address,
+// and we'll figure out if it matches here, if necessary.
+// Caller must hold SDL_LockRWLockForReading(interface_rwlock)!
+static NetworkInterface *GetInterfaceForAddress(NET_Address *addr)
+{
+    if (!addr) {
+        SDL_InvalidParamError("interface");
+        return NULL;
+    } else if (!InterfacesReady()) {
+        return NULL;
+    }
+
+    for (int i = 0; i < num_interfaces; i++) {
+        if (NET_CompareAddresses(addr, interfaces[i].address) == 0) {
+            return &interfaces[i];
+        }
+    }
+
+    return NULL;
+}
+
+static NET_Address *FindBroadcastAddress(struct addrinfo *ainfo, Uint32 *interface_index)
+{
+    NET_Address *retval = NULL;
+
+    NET_Address *iface = CreateSDLNetAddrFromSockAddr(ainfo->ai_addr, (SockLen) ainfo->ai_addrlen);
+    SDL_LockRWLockForReading(interface_rwlock);
+    NetworkInterface *ni = GetInterfaceForAddress(iface);
+    NET_UnrefAddress(iface);
+    if (!ni) {
+        SDL_SetError("Not a network interface address");
+    } else {
+        SDL_assert(ni->address != NULL);
+        *interface_index = ni->index;
+        if (ni->broadcast != NULL) {
+            retval = NET_RefAddress(ni->broadcast);  // we calculated the broadcast address when discovering this interface. It's probably IPv4. We're good to go.
+        } else if (ainfo->ai_family == AF_INET6) {   // we fake this for IPv6 on the all-nodes link-local multicast group.
+            retval = NET_RefAddress(ipv6_broadcast_addr);
+        } else {
+            SDL_SetError("Can't determine broadcast address for this interface");
+        }
+    }
+    SDL_UnlockRWLock(interface_rwlock);
+
+    if (!retval && (ainfo->ai_family == AF_INET6)) {   // we fake this for IPv6 on the all-nodes link-local multicast group.
+        retval = NET_RefAddress(ipv6_broadcast_addr);
+    }
+
+    return retval;
+}
+
+NET_DatagramSocket *NET_CreateDatagramSocket(NET_Address *addr, Uint16 port, SDL_PropertiesID props)
 {
     if (addr && (((NET_Status) SDL_GetAtomicInt(&addr->status)) != NET_SUCCESS)) {
         SDL_SetError("Address is not resolved");  // strictly speaking, this should be a local interface, but a resolved address can fail later.
@@ -1892,6 +1963,11 @@ NET_DatagramSocket *NET_CreateDatagramSocket(NET_Address *addr, Uint16 port)
     sock->addr = addr;
     sock->port = port;
 
+    const int reuseaddr = SDL_GetBooleanProperty(props, NET_PROP_DATAGRAM_SOCKET_REUSEADDR_BOOLEAN, true) ? 1 : 0;
+    sock->allow_broadcast = SDL_GetBooleanProperty(props, NET_PROP_DATAGRAM_SOCKET_ALLOW_BROADCAST_BOOLEAN, false);
+
+    const int bcast = sock->allow_broadcast ? 1 : 0;
+
     int num_handles = 0;
     NET_DatagramSocketHandle *allocated_handles = NULL;
     if (addr != NULL) {
@@ -1937,24 +2013,26 @@ NET_DatagramSocket *NET_CreateDatagramSocket(NET_Address *addr, Uint16 port)
             goto failed;
         }
 
-        sock->handles[sock->num_handles].handle = handle;
-        sock->handles[sock->num_handles].family = ainfo->ai_family;
-        sock->handles[sock->num_handles].protocol = ainfo->ai_protocol;
-        sock->num_handles++;
+        NET_DatagramSocketHandle *socket_handle = &sock->handles[sock->num_handles++];
+
+        SDL_zerop(socket_handle);
+        socket_handle->handle = handle;
+        socket_handle->family = ainfo->ai_family;
+        socket_handle->protocol = ainfo->ai_protocol;
 
         if (MakeSocketNonblocking(handle) < 0) {
             SDL_SetError("Failed to make new socket non-blocking");
             goto failed;
         }
 
-        const int one = 1;
+        setsockopt(handle, SOL_SOCKET, SO_REUSEADDR, (const char *) &reuseaddr, sizeof (reuseaddr));
+        setsockopt(handle, SOL_SOCKET, SO_BROADCAST, (const char *) &bcast, sizeof (bcast));
+
         if (ainfo->ai_family == AF_INET6) {
+            const int one = 1;
             setsockopt(handle, IPPROTO_IPV6, IPV6_V6ONLY, (const char *) &one, sizeof (one));  // if this fails, oh well.
         }
 
-        setsockopt(handle, SOL_SOCKET, SO_REUSEADDR, (const char *) &one, sizeof (one));
-        setsockopt(handle, SOL_SOCKET, SO_BROADCAST, (const char *) &one, sizeof (one));
-
         const int rc = bind(handle, ainfo->ai_addr, (SockLen) ainfo->ai_addrlen);
         if (rc == SOCKET_ERROR) {
             const int err = LastSocketError();
@@ -1962,6 +2040,34 @@ NET_DatagramSocket *NET_CreateDatagramSocket(NET_Address *addr, Uint16 port)
             SetSocketError("Failed to bind socket", err);
             goto failed;
         }
+
+        if (sock->allow_broadcast) {
+            Uint32 interface_index = 0;  // this will stay zero for INADDR6_ANY, to pick a default interface.
+            socket_handle->broadcast = FindBroadcastAddress(ainfo, &interface_index);
+            // if failed but addr==NULL, this is IPv4 and we can't find an interface for INADDR_ANY. We'll broadcast to all interfaces when sending (because 255.255.255.255 doesn't work on Windows).
+            // alternately, we're IPv6 and ipv6_broadcast_addr failed to init for some reason. Just panic and fail here if that happens, I guess.
+            if (!socket_handle->broadcast && (addr || (ainfo->ai_family == AF_INET6))) {
+                SDL_SetError("Failed to determine broadcast address for this interface");
+                goto failed;
+            } else if (ainfo->ai_family == AF_INET6) { // fake broadcast support via multicasting to all-nodes link-local group, ff02::1.
+                SDL_assert(socket_handle->broadcast == ipv6_broadcast_addr);
+                const struct addrinfo *bai = socket_handle->broadcast->ainfo;
+                SDL_assert(bai != NULL);
+                SDL_assert(bai->ai_addr != NULL);
+                SDL_assert(bai->ai_addr->sa_family == AF_INET6);
+                struct ipv6_mreq mreq6;
+                SDL_zero(mreq6);
+                SDL_copyp(&mreq6.ipv6mr_multiaddr, &((struct sockaddr_in6 *) bai->ai_addr)->sin6_addr);
+                mreq6.ipv6mr_interface = interface_index;
+                //SDL_Log("Add membership for %s, index=%d", socket_handle->broadcast->human_readable, (int) interface_index);
+                if (setsockopt(handle, IPPROTO_IPV6, IPV6_JOIN_GROUP, (const char *) &mreq6, (SockLen) sizeof (mreq6)) < 0) {
+                    SDL_SetError("Failed to join all-nodes link-local multicast group for broadcasting");
+                    goto failed;
+                }
+                const int ifidx = (int) interface_index;
+                setsockopt(handle, IPPROTO_IPV6, IPV6_MULTICAST_IF, (const char *) &ifidx, (SockLen) sizeof (ifidx));  // multicast sends go through the same interface.
+            }
+        }
     }
 
     freeaddrinfo(addrwithport);
@@ -1972,6 +2078,7 @@ NET_DatagramSocket *NET_CreateDatagramSocket(NET_Address *addr, Uint16 port)
 failed:
     for (int i = 0; i < sock->num_handles; i++) {
         CloseSocketHandle(sock->handles[i].handle);
+        NET_UnrefAddress(sock->handles[i].broadcast);
     }
     freeaddrinfo(addrwithport);
     SDL_free(allocated_handles);
@@ -1981,29 +2088,94 @@ NET_DatagramSocket *NET_CreateDatagramSocket(NET_Address *addr, Uint16 port)
 
 static NET_Status SendOneDatagram(NET_DatagramSocket *sock, NET_Address *addr, Uint16 port, const void *buf, int buflen)
 {
-    struct addrinfo *addrwithport = MakeAddrInfoWithPort(addr, SOCK_DGRAM, port);
-    if (!addrwithport) {
+    if (addr) {  // unicast to a specific address.
+        struct addrinfo *addrwithport = MakeAddrInfoWithPort(addr, SOCK_DGRAM, port);
+        if (!addrwithport) {
+            return NET_FAILURE;
+        }
+
+        const int family = addrwithport->ai_family;
+        const int protocol = addrwithport->ai_protocol;
+        for (int i = 0; i < sock->num_handles; i++) {
+            const NET_DatagramSocketHandle *handle = &sock->handles[i];
+            if ((handle->family == family) && (handle->protocol == protocol)) {  // !!! FIXME: strictly speaking, this _probably_ just needs to check `family`, right?
+                const int rc = sendto(handle->handle, buf, (size_t) buflen, 0, addrwithport->ai_addr, (SockLen) addrwithport->ai_addrlen);
+                const int err = (rc == SOCKET_ERROR) ? LastSocketError() : 0;
+                freeaddrinfo(addrwithport);
+                if (err != 0) {
+                    return WouldBlock(err) ? NET_WAITING : SetSocketError("Failed to send from socket", err);
+                }
+                SDL_assert(rc == buflen);
+                return NET_SUCCESS;  // !!! FIXME: should we sent to _all_ interfaces in this family?
+            }
+        }
+        SDL_SetError("Unsupported network family in destination address");
         return NET_FAILURE;
+
     }
 
-    const int family = addrwithport->ai_family;
-    const int protocol = addrwithport->ai_protocol;
+    // broadcast (or fake with multicast) this packet.
+    SDL_assert(sock->allow_broadcast);  // we should have checked this in NET_SendDatagram!
+
+    bool all_wouldblock = false;
+    NET_Status retval = NET_FAILURE;
     for (int i = 0; i < sock->num_handles; i++) {
         const NET_DatagramSocketHandle *handle = &sock->handles[i];
-        if ((handle->family == family) && (handle->protocol == protocol)) {  // !!! FIXME: strictly speaking, this _probably_ just needs to check `family`, right?
+
+        if (handle->broadcast) {
+            struct addrinfo *addrwithport = MakeAddrInfoWithPort(handle->broadcast, SOCK_DGRAM, port);
+            if (!addrwithport) {
+                continue;  // oh well, lost UDP packet, I guess.
+            }
+
+            //SDL_Log("Broadcasting on %s ...", handle->broadcast->human_readable);
             const int rc = sendto(handle->handle, buf, (size_t) buflen, 0, addrwithport->ai_addr, (SockLen) addrwithport->ai_addrlen);
             const int err = (rc == SOCKET_ERROR) ? LastSocketError() : 0;
             freeaddrinfo(addrwithport);
-            if (err != 0) {
-                return WouldBlock(err) ? NET_WAITING : SetSocketError("Failed to send from socket", err);
+            if (!err) {
+                retval = NET_SUCCESS;  // it went to at least one interface's broadcast address, we'll call it success.
+            } else {
+                if (!WouldBlock(err)) {
+                    all_wouldblock = false;
+                    SetSocketError("Failed to send from socket", err);  // so there's a clear error message, but keep going, maybe something else works out.
+                }
             }
-            SDL_assert(rc == buflen);
-            return NET_SUCCESS;
+        } else if (!addr) {  // iterate all interfaces for this broadcast.
+            SDL_LockRWLockForReading(interface_rwlock);
+            for (int i = 0; i < num_interfaces; i++) {
+                const NET_Address *bc = interfaces[i].broadcast;
+                if (!bc) { continue; }
+                const struct addrinfo *ainfo = bc->ainfo;
+                SDL_assert(ainfo != NULL);
+                if (ainfo->ai_family != handle->family) { continue; }
+
+                str

(Patch may be truncated, please check the link at the top of this post.)