/* * Copyright (C) 2021 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #import "NetworkRTCUDPSocketCocoa.h" #if USE(LIBWEBRTC) && PLATFORM(COCOA) #include "DataReference.h" #include "LibWebRTCNetworkMessages.h" #include "Logging.h" #include "NWSPI.h" #include #include #include #include #include #include #include #if HAVE(NWPARAMETERS_TRACKER_API) SOFT_LINK_LIBRARY_OPTIONAL(libnetwork) SOFT_LINK_OPTIONAL(libnetwork, nw_parameters_allow_sharing_port_with_listener, void, __cdecl, (nw_parameters_t, nw_listener_t)) #endif namespace WebKit { using namespace WebCore; class NetworkRTCUDPSocketCocoaConnections : public ThreadSafeRefCounted { public: static Ref create(WebCore::LibWebRTCSocketIdentifier identifier, NetworkRTCProvider& provider, const rtc::SocketAddress& address, Ref&& connection, String&& attributedBundleIdentifier, bool isFirstParty, bool isRelayDisabled, const WebCore::RegistrableDomain& domain) { return adoptRef(*new NetworkRTCUDPSocketCocoaConnections(identifier, provider, address, WTFMove(connection), WTFMove(attributedBundleIdentifier), isFirstParty, isRelayDisabled, domain)); } void close(); void setOption(int option, int value); void sendTo(const uint8_t*, size_t, const rtc::SocketAddress&, const rtc::PacketOptions&); void setListeningPort(int); class ConnectionStateTracker : public ThreadSafeRefCounted { public: static Ref create() { return adoptRef(*new ConnectionStateTracker()); } void markAsStopped() { m_isStopped = true; } bool isStopped() const { return m_isStopped; } private: bool m_isStopped { false }; }; private: NetworkRTCUDPSocketCocoaConnections(WebCore::LibWebRTCSocketIdentifier, NetworkRTCProvider&, const rtc::SocketAddress&, Ref&&, String&& attributedBundleIdentifier, bool isFirstParty, bool isRelayDisabled, const WebCore::RegistrableDomain&); std::pair, Ref> createNWConnection(const rtc::SocketAddress&); void setupNWConnection(nw_connection_t, ConnectionStateTracker&, const rtc::SocketAddress&); void configureParameters(nw_parameters_t, nw_ip_version_t); WebCore::LibWebRTCSocketIdentifier m_identifier; Ref m_connection; bool m_isFirstParty { false }; bool m_isKnownTracker { false }; bool m_shouldBypassRelay { false }; CString m_sourceApplicationBundleIdentifier; std::optional m_sourceApplicationAuditToken; String m_attributedBundleIdentifier; rtc::SocketAddress m_address; RetainPtr m_nwListener; Lock m_nwConnectionsLock; bool m_isClosed WTF_GUARDED_BY_LOCK(m_nwConnectionsLock) { false }; HashMap, RefPtr>> m_nwConnections WTF_GUARDED_BY_LOCK(m_nwConnectionsLock); }; static dispatch_queue_t udpSocketQueue() { static dispatch_queue_t queue; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ queue = dispatch_queue_create("WebRTC UDP socket queue", DISPATCH_QUEUE_CONCURRENT); }); return queue; } NetworkRTCUDPSocketCocoa::NetworkRTCUDPSocketCocoa(WebCore::LibWebRTCSocketIdentifier identifier, NetworkRTCProvider& rtcProvider, const rtc::SocketAddress& address, Ref&& connection, String&& attributedBundleIdentifier, bool isFirstParty, bool isRelayDisabled, const WebCore::RegistrableDomain& domain) : m_rtcProvider(rtcProvider) , m_identifier(identifier) , m_connections(NetworkRTCUDPSocketCocoaConnections::create(identifier, rtcProvider, address, WTFMove(connection), WTFMove(attributedBundleIdentifier), isFirstParty, isRelayDisabled, domain)) { } NetworkRTCUDPSocketCocoa::~NetworkRTCUDPSocketCocoa() { } void NetworkRTCUDPSocketCocoa::close() { m_connections->close(); m_rtcProvider.takeSocket(m_identifier); } void NetworkRTCUDPSocketCocoa::setListeningPort(int port) { m_connections->setListeningPort(port); } void NetworkRTCUDPSocketCocoa::setOption(int option, int value) { m_connections->setOption(option, value); } void NetworkRTCUDPSocketCocoa::sendTo(const uint8_t* data, size_t size, const rtc::SocketAddress& address, const rtc::PacketOptions& options) { m_connections->sendTo(data, size, address, options); } static rtc::SocketAddress socketAddressFromIncomingConnection(nw_connection_t connection) { auto endpoint = adoptNS(nw_connection_copy_endpoint(connection)); auto type = nw_endpoint_get_type(endpoint.get()); if (type == nw_endpoint_type_address) { auto* ipAddress = nw_endpoint_copy_address_string(endpoint.get()); rtc::SocketAddress remoteAddress { ipAddress, nw_endpoint_get_port(endpoint.get()) }; free(ipAddress); return remoteAddress; } return rtc::SocketAddress { nw_endpoint_get_hostname(endpoint.get()), nw_endpoint_get_port(endpoint.get()) }; } static inline bool isNat64IPAddress(const rtc::IPAddress& ip) { if (ip.family() != AF_INET) return false; struct ifaddrs* interfaces; if (auto error = getifaddrs(&interfaces)) return true; std::unique_ptr toBeFreed(interfaces); for (auto* interface = interfaces; interface; interface = interface->ifa_next) { if (interface->ifa_addr->sa_family != AF_INET) continue; rtc::IPAddress interfaceAddress { reinterpret_cast(interface->ifa_addr)->sin_addr }; if (ip != interfaceAddress) continue; return nw_nat64_does_interface_index_support_nat64(if_nametoindex(interface->ifa_name)); } return false; } static std::string computeHostAddress(const rtc::SocketAddress& address) { if (address.ipaddr().IsNil()) return address.hostname(); if (!isNat64IPAddress(address.ipaddr())) return address.ipaddr().ToString(); return "0.0.0.0"; } NetworkRTCUDPSocketCocoaConnections::NetworkRTCUDPSocketCocoaConnections(WebCore::LibWebRTCSocketIdentifier identifier, NetworkRTCProvider& rtcProvider, const rtc::SocketAddress& address, Ref&& connection, String&& attributedBundleIdentifier, bool isFirstParty, bool isRelayDisabled, const WebCore::RegistrableDomain& domain) : m_identifier(identifier) , m_connection(WTFMove(connection)) , m_isFirstParty(isFirstParty) , m_isKnownTracker(isKnownTracker(domain)) , m_shouldBypassRelay(isRelayDisabled) , m_sourceApplicationBundleIdentifier(rtcProvider.applicationBundleIdentifier()) , m_sourceApplicationAuditToken(rtcProvider.sourceApplicationAuditToken()) , m_attributedBundleIdentifier(WTFMove(attributedBundleIdentifier)) { auto parameters = adoptNS(nw_parameters_create_secure_udp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION)); { auto hostAddress = computeHostAddress(address); auto localEndpoint = adoptNS(nw_endpoint_create_host_with_numeric_port(hostAddress.c_str(), 0)); m_address = { nw_endpoint_get_hostname(localEndpoint.get()), nw_endpoint_get_port(localEndpoint.get()) }; nw_parameters_set_local_endpoint(parameters.get(), localEndpoint.get()); } configureParameters(parameters.get(), address.family() == AF_INET ? nw_ip_version_4 : nw_ip_version_6); m_nwListener = adoptNS(nw_listener_create(parameters.get())); nw_listener_set_queue(m_nwListener.get(), udpSocketQueue()); // The callback holds a reference to the nw_listener and we clear it when going in nw_listener_state_cancelled state, which is triggered when closing the socket. nw_listener_set_state_changed_handler(m_nwListener.get(), makeBlockPtr([nwListener = m_nwListener, connection = m_connection.copyRef(), protectedRTCProvider = Ref { rtcProvider }, identifier = m_identifier](nw_listener_state_t state, nw_error_t error) mutable { switch (state) { case nw_listener_state_invalid: case nw_listener_state_waiting: break; case nw_listener_state_ready: protectedRTCProvider->doSocketTaskOnRTCNetworkThread(identifier, [port = nw_listener_get_port(nwListener.get())](auto& socket) mutable { auto& udpSocket = static_cast(socket); udpSocket.setListeningPort(port); }); break; case nw_listener_state_failed: RELEASE_LOG_ERROR(WebRTC, "NetworkRTCUDPSocketCocoaConnections failed with error %d", error ? nw_error_get_error_code(error) : 0); protectedRTCProvider->callOnRTCNetworkThread([protectedRTCProvider, identifier] { protectedRTCProvider->closeSocket(identifier); }); connection->send(Messages::LibWebRTCNetwork::SignalClose(identifier, -1), 0); break; case nw_listener_state_cancelled: RELEASE_LOG(WebRTC, "NetworkRTCUDPSocketCocoaConnections cancelled listener %" PRIu64, identifier.toUInt64()); nwListener.clear(); break; } }).get()); nw_listener_set_new_connection_handler(m_nwListener.get(), makeBlockPtr([protectedThis = Ref { *this }](nw_connection_t nwConnection) { Locker locker { protectedThis->m_nwConnectionsLock }; if (protectedThis->m_isClosed) return; auto remoteAddress = socketAddressFromIncomingConnection(nwConnection); ASSERT(remoteAddress != HashTraits::emptyValue() && !HashTraits::isDeletedValue(remoteAddress)); auto connectionStateTracker = ConnectionStateTracker::create(); protectedThis->setupNWConnection(nwConnection, connectionStateTracker.get(), remoteAddress); protectedThis->m_nwConnections.set(remoteAddress, std::make_pair(nwConnection, WTFMove(connectionStateTracker))); }).get()); nw_listener_start(m_nwListener.get()); } void NetworkRTCUDPSocketCocoaConnections::setListeningPort(int port) { m_address.SetPort(port); m_connection->send(Messages::LibWebRTCNetwork::SignalAddressReady(m_identifier, RTCNetwork::SocketAddress(m_address)), 0); } void NetworkRTCUDPSocketCocoaConnections::configureParameters(nw_parameters_t parameters, nw_ip_version_t version) { auto protocolStack = adoptNS(nw_parameters_copy_default_protocol_stack(parameters)); auto options = adoptNS(nw_protocol_stack_copy_internet_protocol(protocolStack.get())); nw_ip_options_set_version(options.get(), version); setNWParametersApplicationIdentifiers(parameters, m_sourceApplicationBundleIdentifier.data(), m_sourceApplicationAuditToken, m_attributedBundleIdentifier); setNWParametersTrackerOptions(parameters, m_shouldBypassRelay, m_isFirstParty, m_isKnownTracker); nw_parameters_set_reuse_local_address(parameters, true); } void NetworkRTCUDPSocketCocoaConnections::close() { Locker locker { m_nwConnectionsLock }; m_isClosed = true; for (auto& nwConnection : m_nwConnections.values()) { nwConnection.second->markAsStopped(); nw_connection_cancel(nwConnection.first.get()); } m_nwConnections.clear(); nw_listener_cancel(m_nwListener.get()); m_nwListener = nullptr; } void NetworkRTCUDPSocketCocoaConnections::setOption(int, int) { // FIXME: Validate this is not needed. } static inline void processUDPData(RetainPtr&& nwConnection, Ref connectionStateTracker, int errorCode, Function&& processData) { auto nwConnectionReference = nwConnection.get(); nw_connection_receive(nwConnectionReference, 1, std::numeric_limits::max(), makeBlockPtr([nwConnection = WTFMove(nwConnection), processData = WTFMove(processData), errorCode, connectionStateTracker = WTFMove(connectionStateTracker)](dispatch_data_t content, nw_content_context_t, bool, nw_error_t error) mutable { if (content) { dispatch_data_apply(content, makeBlockPtr([&](dispatch_data_t, size_t, const void* data, size_t size) { processData(static_cast(data), size); return true; }).get()); } if (connectionStateTracker->isStopped()) return; if (error && errorCode != nw_error_get_error_code(error)) { errorCode = nw_error_get_error_code(error); RELEASE_LOG_ERROR(WebRTC, "NetworkRTCUDPSocketCocoaConnections failed processing UDP data with error %d", errorCode); } processUDPData(WTFMove(nwConnection), WTFMove(connectionStateTracker), errorCode, WTFMove(processData)); }).get()); } std::pair, Ref> NetworkRTCUDPSocketCocoaConnections::createNWConnection(const rtc::SocketAddress& remoteAddress) { auto parameters = adoptNS(nw_parameters_create_secure_udp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION)); { auto hostAddress = m_address.ipaddr().ToString(); if (m_address.ipaddr().IsNil()) hostAddress = m_address.hostname(); // rdar://80176676: we workaround local loop port reuse by using 0 instead of m_address.port() when nw_parameters_allow_sharing_port_with_listener is not available. uint16_t port = 0; #if HAVE(NWPARAMETERS_TRACKER_API) if (nw_parameters_allow_sharing_port_with_listenerPtr()) { nw_parameters_allow_sharing_port_with_listenerPtr()(parameters.get(), m_nwListener.get()); port = m_address.port(); } #endif auto localEndpoint = adoptNS(nw_endpoint_create_host_with_numeric_port(hostAddress.c_str(), port)); nw_parameters_set_local_endpoint(parameters.get(), localEndpoint.get()); } configureParameters(parameters.get(), remoteAddress.family() == AF_INET ? nw_ip_version_4 : nw_ip_version_6); auto remoteHostAddress = remoteAddress.ipaddr().ToString(); if (remoteAddress.ipaddr().IsNil()) remoteHostAddress = remoteAddress.hostname(); auto host = adoptNS(nw_endpoint_create_host(remoteHostAddress.c_str(), String::number(remoteAddress.port()).utf8().data())); auto nwConnection = adoptNS(nw_connection_create(host.get(), parameters.get())); auto connectionStateTracker = ConnectionStateTracker::create(); setupNWConnection(nwConnection.get(), connectionStateTracker.get(), remoteAddress); return std::make_pair(WTFMove(nwConnection), WTFMove(connectionStateTracker)); } void NetworkRTCUDPSocketCocoaConnections::setupNWConnection(nw_connection_t nwConnection, ConnectionStateTracker& connectionStateTracker, const rtc::SocketAddress& remoteAddress) { nw_connection_set_queue(nwConnection, udpSocketQueue()); nw_connection_set_state_changed_handler(nwConnection, makeBlockPtr([connectionStateTracker = Ref { connectionStateTracker }](nw_connection_state_t state, _Nullable nw_error_t error) { RELEASE_LOG_ERROR_IF(state == nw_connection_state_failed, WebRTC, "NetworkRTCUDPSocketCocoaConnections connection failed with error %d", error ? nw_error_get_error_code(error) : 0); if (state == nw_connection_state_failed || state == nw_connection_state_cancelled) connectionStateTracker->markAsStopped(); }).get()); processUDPData(nwConnection, Ref { connectionStateTracker }, 0, [identifier = m_identifier, connection = m_connection.copyRef(), ip = remoteAddress.ipaddr(), port = remoteAddress.port()](auto* message, auto size) mutable { IPC::DataReference data(message, size); connection->send(Messages::LibWebRTCNetwork::SignalReadPacket { identifier, data, RTCNetwork::IPAddress(ip), port, rtc::TimeMillis() * 1000 }, 0); }); nw_connection_start(nwConnection); } void NetworkRTCUDPSocketCocoaConnections::sendTo(const uint8_t* data, size_t size, const rtc::SocketAddress& remoteAddress, const rtc::PacketOptions& options) { bool isInCorrectValue = (remoteAddress == HashTraits::emptyValue()) || HashTraits::isDeletedValue(remoteAddress); ASSERT(!isInCorrectValue); if (isInCorrectValue) return; nw_connection_t nwConnection; { Locker locker { m_nwConnectionsLock }; nwConnection = m_nwConnections.ensure(remoteAddress, [this, &remoteAddress] { return createNWConnection(remoteAddress); }).iterator->value.first.get(); } auto value = adoptNS(dispatch_data_create(data, size, nullptr, DISPATCH_DATA_DESTRUCTOR_DEFAULT)); nw_connection_send(nwConnection, value.get(), NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, true, makeBlockPtr([identifier = m_identifier, connection = m_connection.copyRef(), options](_Nullable nw_error_t error) { RELEASE_LOG_ERROR_IF(error, WebRTC, "NetworkRTCUDPSocketCocoaConnections::sendTo failed with error %d", error ? nw_error_get_error_code(error) : 0); connection->send(Messages::LibWebRTCNetwork::SignalSentPacket { identifier, options.packet_id, rtc::TimeMillis() }, 0); }).get()); } } // namespace WebKit #endif // USE(LIBWEBRTC) && PLATFORM(COCOA)