Skip to content
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
108 changes: 106 additions & 2 deletions include/webcraft/async/io/socket.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include "core.hpp"
#include <webcraft/async/fire_and_forget_task.hpp>
#include <stdexcept>

namespace webcraft::async::io::socket
{
Expand All @@ -16,6 +17,81 @@ namespace webcraft::async::io::socket
uint16_t port;
};

/// Placeholder for options when joining a multicast group. Currently empty: an instance
/// of this type indicates default multicast join behavior. Fields may be added in the future.
struct multicast_join_options
{
};

namespace detail
{
/// Returns true if the given address string is a valid IPv4 or IPv6 multicast address.
inline bool is_multicast_address(const std::string &addr)
{
if (addr.empty()) return false;
if (addr.find('.') != std::string::npos)
{
// IPv4: 224.0.0.0 - 239.255.255.255
unsigned a = 0, b = 0, c = 0, d = 0;
int n = 0;
if (std::sscanf(addr.c_str(), "%u.%u.%u.%u%n", &a, &b, &c, &d, &n) != 4) return false;
if (addr.size() != static_cast<std::size_t>(n)) return false;
if (a > 255u || b > 255u || c > 255u || d > 255u) return false;
if (a < 224u || a > 239u) return false;
return true;
}
if (addr.find(':') != std::string::npos)
{
// IPv6: ff00::/8 — first 16-bit segment must be 0xff00–0xffff
std::size_t i = 0;
while (i < addr.size() && addr[i] == ':') ++i;
while (i < addr.size())
{
std::size_t start = i;
while (i < addr.size() && addr[i] != ':')
{
char ch = addr[i];
if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F'))
++i;
else
return false;
}
if (i > start)
{
if (i - start >= 2u && i - start <= 4u)
{
if ((addr[start] == 'f' || addr[start] == 'F') && (addr[start + 1] == 'f' || addr[start + 1] == 'F'))
return true;
}
return false;
}
if (i < addr.size()) ++i;
}
return false;
}
Comment on lines +43 to +71
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The IPv6 multicast address validation logic is incomplete and will incorrectly validate many IPv6 addresses. The current implementation only checks if the first hexadecimal segment starts with 'ff', but this has several issues:

  1. It only checks the first non-empty segment encountered, not necessarily the first segment of the address
  2. It returns true immediately upon finding 'ff', without validating the rest of the address format
  3. It doesn't handle IPv6 compression (::) correctly
  4. Valid multicast addresses like "ff02::1" would fail because the implementation doesn't properly handle the :: compression

For correct validation, consider using inet_pton(AF_INET6, ...) followed by IN6_IS_ADDR_MULTICAST() macro (which checks if the first byte is 0xff), similar to how it's done in the join_group implementation in async_udp.cpp. This would be more robust and consistent with the runtime validation.

Copilot uses AI. Check for mistakes.
return false;
}
}

/// Represents a multicast group address. Use resolve() to create from a string (e.g. "239.255.0.1").
struct multicast_group
{
std::string host; ///< Multicast group address (e.g. "239.255.0.1")
uint16_t port{0}; ///< Port used when sending to the group; must be set to the desired (non-zero) UDP port before calling send functions.

/// Resolve a multicast group from an address string (IPv4 or IPv6 multicast address).
/// \throws std::invalid_argument if addr is not a valid multicast address.
static multicast_group resolve(std::string_view addr)
{
std::string s(addr);
if (!detail::is_multicast_address(s))
throw std::invalid_argument("Not a multicast address: " + s);
multicast_group g;
g.host = std::move(s);
return g;
}
};

enum class ip_version
{
IPv4,
Expand All @@ -30,7 +106,6 @@ namespace webcraft::async::io::socket

namespace detail
{

class tcp_descriptor_base
{
public:
Expand Down Expand Up @@ -77,6 +152,11 @@ namespace webcraft::async::io::socket

virtual task<size_t> recvfrom(std::span<char> buffer, connection_info &info) = 0;
virtual task<size_t> sendto(std::span<const char> buffer, const connection_info &info) = 0;

/// Join a multicast group. Optional; no-op if not supported (e.g. mock).
virtual void join_group(const multicast_group &group, const multicast_join_options &opts) { (void)group; (void)opts; }
/// Leave a multicast group.
virtual void leave_group(const multicast_group &group) { (void)group; }
};

std::shared_ptr<tcp_socket_descriptor> make_tcp_socket_descriptor();
Expand Down Expand Up @@ -302,7 +382,7 @@ namespace webcraft::async::io::socket

task<tcp_socket> accept()
{
co_return co_await descriptor->accept();
co_return tcp_socket(co_await descriptor->accept());
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

This fix to tcp_listener::accept() is not mentioned in the PR description. The change adds an explicit tcp_socket constructor call that was missing, which would have caused a compilation error (cannot convert std::shared_ptr<tcp_socket_descriptor> to tcp_socket without calling the constructor).

This appears to be an unrelated bug fix that was included in this PR. Consider either:

  1. Updating the PR description to mention this fix, or
  2. Moving this fix to a separate PR if it's not directly related to the multicast functionality

Copilot uses AI. Check for mistakes.
}

task<void> close()
Expand Down Expand Up @@ -341,6 +421,16 @@ namespace webcraft::async::io::socket
descriptor->bind(info);
}

void join(const multicast_group &group)
{
descriptor->join_group(group, multicast_join_options{});
}

void leave(const multicast_group &group)
{
descriptor->leave_group(group);
}

task<size_t> recvfrom(std::span<char> buffer, connection_info &info)
{
return descriptor->recvfrom(buffer, info);
Expand All @@ -350,8 +440,17 @@ namespace webcraft::async::io::socket
{
return descriptor->sendto(buffer, info);
}

task<size_t> sendto(std::span<const char> buffer, const multicast_group &group)
{
connection_info info{group.host, group.port};
return descriptor->sendto(buffer, info);
}
};

/// Alias for UDP socket used in multicast contexts (same type; join/leave/sendto(group) available).
using multicast_socket = udp_socket;

inline tcp_socket make_tcp_socket()
{
return tcp_socket(detail::make_tcp_socket_descriptor());
Expand All @@ -366,4 +465,9 @@ namespace webcraft::async::io::socket
{
return udp_socket(detail::make_udp_socket_descriptor(version));
}

inline multicast_socket make_multicast_socket(std::optional<ip_version> version = std::nullopt)
{
return make_udp_socket(version);
}
}
66 changes: 66 additions & 0 deletions src/webcraft/async_udp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <webcraft/async/runtime/macos.event.hpp>
#include <webcraft/async/runtime/linux.event.hpp>
#include <cstdio>
#include <stdexcept>
#include <webcraft/async/thread_pool.hpp>
#include <webcraft/async/async_event.hpp>
#include <webcraft/net/util.hpp>
Expand All @@ -31,6 +32,13 @@ using namespace webcraft::async::io::socket::detail;
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <MSWSock.h>
// Windows uses IPV6_ADD_MEMBERSHIP/IPV6_DROP_MEMBERSHIP; POSIX uses IPV6_JOIN_GROUP/IPV6_LEAVE_GROUP.
#ifndef IPV6_JOIN_GROUP
#define IPV6_JOIN_GROUP IPV6_ADD_MEMBERSHIP
#endif
#ifndef IPV6_LEAVE_GROUP
#define IPV6_LEAVE_GROUP IPV6_DROP_MEMBERSHIP
#endif

#elif defined(__APPLE__)

Expand Down Expand Up @@ -795,6 +803,64 @@ class io_uring_udp_socket_descriptor : public webcraft::async::io::socket::detai

co_return bytes_sent;
}

void join_group(const webcraft::async::io::socket::multicast_group &group, const webcraft::async::io::socket::multicast_join_options &) override
{
if (socket < 0) return;
in_addr maddr4;
if (inet_pton(AF_INET, group.host.c_str(), &maddr4) == 1)
{
if (!IN_MULTICAST(ntohl(maddr4.s_addr)))
throw std::invalid_argument("Not a multicast address: " + group.host);
struct ip_mreq mreq{};
mreq.imr_multiaddr = maddr4;
mreq.imr_interface.s_addr = INADDR_ANY;
if (setsockopt(socket, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0)
{
std::error_code ec(errno, std::system_category());
throw std::system_error(ec, "Failed to join IPv4 multicast group");
}
return;
}
struct in6_addr maddr6;
if (inet_pton(AF_INET6, group.host.c_str(), &maddr6) == 1)
{
if (!IN6_IS_ADDR_MULTICAST(&maddr6))
throw std::invalid_argument("Not a multicast address: " + group.host);
struct ipv6_mreq mreq6{};
mreq6.ipv6mr_multiaddr = maddr6;
mreq6.ipv6mr_interface = 0;
if (setsockopt(socket, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mreq6, sizeof(mreq6)) < 0)
{
std::error_code ec(errno, std::system_category());
throw std::system_error(ec, "Failed to join IPv6 multicast group");
}
return;
}
throw std::invalid_argument("Invalid multicast address: " + group.host);
}

void leave_group(const webcraft::async::io::socket::multicast_group &group) override
{
if (socket < 0) return;
in_addr maddr4;
if (inet_pton(AF_INET, group.host.c_str(), &maddr4) == 1)
{
struct ip_mreq mreq{};
mreq.imr_multiaddr = maddr4;
mreq.imr_interface.s_addr = INADDR_ANY;
setsockopt(socket, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq));
return;
}
struct in6_addr maddr6;
if (inet_pton(AF_INET6, group.host.c_str(), &maddr6) == 1)
{
struct ipv6_mreq mreq6{};
mreq6.ipv6mr_multiaddr = maddr6;
mreq6.ipv6mr_interface = 0;
setsockopt(socket, IPPROTO_IPV6, IPV6_LEAVE_GROUP, &mreq6, sizeof(mreq6));
}
Comment on lines +843 to +862
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The leave_group implementation does not validate that the provided address is a valid multicast address before attempting to leave. While join_group validates using IN_MULTICAST and IN6_IS_ADDR_MULTICAST, leave_group silently ignores invalid addresses. This inconsistency could mask programming errors where invalid addresses are passed to leave_group.

For consistency and to help catch errors early, consider adding the same validation in leave_group that exists in join_group (lines 813-814 and 828-829).

Copilot uses AI. Check for mistakes.
Comment on lines +843 to +862
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The error handling is inconsistent between join_group and leave_group. The join_group method throws std::system_error when setsockopt fails (lines 818-822, 833-837), but leave_group silently ignores setsockopt failures (lines 852, 861).

This inconsistency could make debugging difficult. If leaving a multicast group fails, the application should likely be notified, especially since the join operation is careful to report errors. Consider either:

  1. Making leave_group throw on setsockopt failure for consistency with join_group, or
  2. Documenting why failures are acceptable in leave_group (e.g., if the socket is being closed anyway)

Copilot uses AI. Check for mistakes.
}
Comment on lines +807 to +863
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The join_group and leave_group methods are only implemented for the Linux io_uring_udp_socket_descriptor class. The Windows (iocp_udp_socket_descriptor) and macOS (kqueue_udp_socket_descriptor) implementations do not have these methods, which means they will fall back to the no-op default implementation in the base class. This means multicast functionality will silently fail on Windows and macOS platforms.

Either:

  1. Implement join_group and leave_group for Windows and macOS (recommended), or
  2. Document this platform limitation clearly in the multicast_group API documentation and throw an appropriate exception from the default implementations rather than silently doing nothing.

Copilot uses AI. Check for mistakes.
};

std::shared_ptr<webcraft::async::io::socket::detail::udp_socket_descriptor> webcraft::async::io::socket::detail::make_udp_socket_descriptor(std::optional<webcraft::async::io::socket::ip_version> version)
Expand Down
Loading
Loading