diff options
Diffstat (limited to 'dom/media/webrtc/transport/test_nr_socket.cpp')
-rw-r--r-- | dom/media/webrtc/transport/test_nr_socket.cpp | 1135 |
1 files changed, 1135 insertions, 0 deletions
diff --git a/dom/media/webrtc/transport/test_nr_socket.cpp b/dom/media/webrtc/transport/test_nr_socket.cpp new file mode 100644 index 0000000000..064013a8b7 --- /dev/null +++ b/dom/media/webrtc/transport/test_nr_socket.cpp @@ -0,0 +1,1135 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* + */ + +/* +Based partially on original code from nICEr and nrappkit. + +nICEr copyright: + +Copyright (c) 2007, Adobe Systems, Incorporated +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* 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. + +* Neither the name of Adobe Systems, Network Resonance nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT +OWNER OR 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. + + +nrappkit copyright: + + Copyright (C) 2001-2003, Network Resonance, Inc. + Copyright (C) 2006, Network Resonance, 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. + 3. Neither the name of Network Resonance, Inc. nor the name of any + contributors to this software may be used to endorse or promote + products derived from this software without specific prior written + permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT OWNER OR 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. + + + ekr@rtfm.com Thu Dec 20 20:14:49 2001 +*/ + +// Original author: bcampen@mozilla.com [:bwc] + +extern "C" { +#include "stun_msg.h" // for NR_STUN_MAX_MESSAGE_SIZE +#include "async_wait.h" +#include "async_timer.h" +#include "nr_socket.h" +#include "stun.h" +#include "transport_addr.h" +} + +#include "mozilla/RefPtr.h" +#include "test_nr_socket.h" + +namespace mozilla { + +static int test_nat_socket_create(void* obj, nr_transport_addr* addr, + nr_socket** sockp) { + RefPtr<NrSocketBase> sock = new TestNrSocket(static_cast<TestNat*>(obj)); + + int r, _status; + + r = sock->create(addr); + if (r) ABORT(r); + + r = nr_socket_create_int(static_cast<void*>(sock), sock->vtbl(), sockp); + if (r) ABORT(r); + + _status = 0; + + { + // We will release this reference in destroy(), not exactly the normal + // ownership model, but it is what it is. + NrSocketBase* dummy = sock.forget().take(); + (void)dummy; + } + +abort: + return _status; +} + +static int test_nat_socket_factory_destroy(void** obj) { + TestNat* nat = static_cast<TestNat*>(*obj); + *obj = nullptr; + nat->Release(); + return 0; +} + +static nr_socket_factory_vtbl test_nat_socket_factory_vtbl = { + test_nat_socket_create, test_nat_socket_factory_destroy}; + +/* static */ +TestNat::NatBehavior TestNat::ToNatBehavior(const std::string& type) { + if (type.empty() || !type.compare("ENDPOINT_INDEPENDENT")) { + return TestNat::ENDPOINT_INDEPENDENT; + } + if (!type.compare("ADDRESS_DEPENDENT")) { + return TestNat::ADDRESS_DEPENDENT; + } + if (!type.compare("PORT_DEPENDENT")) { + return TestNat::PORT_DEPENDENT; + } + + MOZ_ASSERT(false, "Invalid NAT behavior"); + return TestNat::ENDPOINT_INDEPENDENT; +} + +bool TestNat::has_port_mappings() const { + for (TestNrSocket* sock : sockets_) { + if (sock->has_port_mappings()) { + return true; + } + } + return false; +} + +bool TestNat::is_my_external_tuple(const nr_transport_addr& addr) const { + for (TestNrSocket* sock : sockets_) { + if (sock->is_my_external_tuple(addr)) { + return true; + } + } + + return false; +} + +bool TestNat::is_an_internal_tuple(const nr_transport_addr& addr) const { + for (TestNrSocket* sock : sockets_) { + nr_transport_addr addr_behind_nat; + if (sock->getaddr(&addr_behind_nat)) { + MOZ_CRASH("TestNrSocket::getaddr failed!"); + } + + if (!nr_transport_addr_cmp(&addr, &addr_behind_nat, + NR_TRANSPORT_ADDR_CMP_MODE_ALL)) { + return true; + } + } + return false; +} + +int TestNat::create_socket_factory(nr_socket_factory** factorypp) { + int r = nr_socket_factory_create_int(this, &test_nat_socket_factory_vtbl, + factorypp); + if (!r) { + AddRef(); + } + return r; +} + +void TestNat::set_proxy_config( + std::shared_ptr<NrSocketProxyConfig> aProxyConfig) { + proxy_config_ = std::move(aProxyConfig); +} + +TestNrSocket::TestNrSocket(TestNat* nat) + : nat_(nat), tls_(false), timer_handle_(nullptr) { + nat_->insert_socket(this); +} + +TestNrSocket::~TestNrSocket() { nat_->erase_socket(this); } + +RefPtr<NrSocketBase> TestNrSocket::create_external_socket( + const nr_transport_addr& dest_addr) const { + MOZ_ASSERT(nat_->enabled_); + MOZ_ASSERT(!nat_->is_an_internal_tuple(dest_addr)); + + int r; + nr_transport_addr nat_external_addr; + + // Open the socket on an arbitrary port, on the same address. + if ((r = nr_transport_addr_copy(&nat_external_addr, + &internal_socket_->my_addr()))) { + r_log(LOG_GENERIC, LOG_CRIT, "%s: Failure in nr_transport_addr_copy: %d", + __FUNCTION__, r); + return nullptr; + } + + if ((r = nr_transport_addr_set_port(&nat_external_addr, 0))) { + r_log(LOG_GENERIC, LOG_CRIT, + "%s: Failure in nr_transport_addr_set_port: %d", __FUNCTION__, r); + return nullptr; + } + + RefPtr<NrSocketBase> external_socket; + r = NrSocketBase::CreateSocket(&nat_external_addr, &external_socket, + nat_->proxy_config_); + + if (r) { + r_log(LOG_GENERIC, LOG_CRIT, "%s: Failure in NrSocket::create: %d", + __FUNCTION__, r); + return nullptr; + } + + return external_socket; +} + +int TestNrSocket::create(nr_transport_addr* addr) { + tls_ = addr->tls; + + r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %p create %s", this, + addr->as_string); + return NrSocketBase::CreateSocket(addr, &internal_socket_, nullptr); +} + +int TestNrSocket::getaddr(nr_transport_addr* addrp) { + return internal_socket_->getaddr(addrp); +} + +void TestNrSocket::close() { + r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %p %s closing", this, + internal_socket_->my_addr().as_string); + if (timer_handle_) { + NR_async_timer_cancel(timer_handle_); + timer_handle_ = nullptr; + } + internal_socket_->close(); + for (RefPtr<PortMapping>& port_mapping : port_mappings_) { + port_mapping->external_socket_->close(); + } +} + +int TestNrSocket::listen(int backlog) { + MOZ_ASSERT(internal_socket_->my_addr().protocol == IPPROTO_TCP); + r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %p %s listening", this, + internal_socket_->my_addr().as_string); + + return internal_socket_->listen(backlog); +} + +int TestNrSocket::accept(nr_transport_addr* addrp, nr_socket** sockp) { + MOZ_ASSERT(internal_socket_->my_addr().protocol == IPPROTO_TCP); + int r = internal_socket_->accept(addrp, sockp); + if (r) { + return r; + } + + if (nat_->enabled_ && !nat_->is_an_internal_tuple(*addrp)) { + nr_socket_destroy(sockp); + return R_IO_ERROR; + } + + return 0; +} + +void TestNrSocket::process_delayed_cb(NR_SOCKET s, int how, void* cb_arg) { + DeferredPacket* op = static_cast<DeferredPacket*>(cb_arg); + op->socket_->timer_handle_ = nullptr; + r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %s sending delayed STUN response", + op->internal_socket_->my_addr().as_string); + op->internal_socket_->sendto(op->buffer_.data(), op->buffer_.len(), + op->flags_, &op->to_); + + delete op; +} + +int TestNrSocket::sendto(const void* msg, size_t len, int flags, + const nr_transport_addr* to) { + MOZ_ASSERT(internal_socket_->my_addr().protocol != IPPROTO_TCP); + r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %p %s %s", this, __FUNCTION__, + to->as_string); + + if (nat_->nat_delegate_ && + nat_->nat_delegate_->on_sendto(nat_, msg, len, flags, to)) { + return nat_->error_code_for_drop_; + } + + UCHAR* buf = static_cast<UCHAR*>(const_cast<void*>(msg)); + if (nat_->block_stun_ && nr_is_stun_message(buf, len)) { + return nat_->error_code_for_drop_; + } + + if (nr_is_stun_request_message(buf, len) && + maybe_send_fake_response(buf, len, to)) { + return 0; + } + + /* TODO: improve the functionality of this in bug 1253657 */ + if (!nat_->enabled_ || nat_->is_an_internal_tuple(*to)) { + if (nat_->delay_stun_resp_ms_ && nr_is_stun_response_message(buf, len)) { + NR_ASYNC_TIMER_SET( + nat_->delay_stun_resp_ms_, process_delayed_cb, + new DeferredPacket(this, msg, len, flags, to, internal_socket_), + &timer_handle_); + return 0; + } + return internal_socket_->sendto(msg, len, flags, to); + } + + destroy_stale_port_mappings(); + + if (to->protocol == IPPROTO_UDP && nat_->block_udp_) { + return nat_->error_code_for_drop_; + } + + // Choose our port mapping based on our most selective criteria + PortMapping* port_mapping = get_port_mapping( + *to, std::max(nat_->filtering_type_, nat_->mapping_type_)); + + if (!port_mapping) { + // See if we have already made the external socket we need to use. + PortMapping* similar_port_mapping = + get_port_mapping(*to, nat_->mapping_type_); + RefPtr<NrSocketBase> external_socket; + + if (similar_port_mapping) { + external_socket = similar_port_mapping->external_socket_; + } else { + external_socket = create_external_socket(*to); + if (!external_socket) { + MOZ_ASSERT(false); + return R_INTERNAL; + } + } + + port_mapping = create_port_mapping(*to, external_socket); + port_mappings_.push_back(port_mapping); + + if (poll_flags() & PR_POLL_READ) { + // Make sure the new port mapping is ready to receive traffic if the + // TestNrSocket is already waiting. + port_mapping->async_wait(NR_ASYNC_WAIT_READ, socket_readable_callback, + this, (char*)__FUNCTION__, __LINE__); + } + } + + // We probably don't want to propagate the flags, since this is a simulated + // external IP address. + return port_mapping->sendto(msg, len, *to); +} + +int TestNrSocket::recvfrom(void* buf, size_t maxlen, size_t* len, int flags, + nr_transport_addr* from) { + MOZ_ASSERT(internal_socket_->my_addr().protocol != IPPROTO_TCP); + + if (!read_buffer_.empty()) { + UdpPacket& packet = read_buffer_.front(); + *len = std::min(maxlen, packet.buffer_->len()); + memcpy(buf, packet.buffer_->data(), *len); + nr_transport_addr_copy(from, &packet.remote_address_); + read_buffer_.pop_front(); + return 0; + } + + int r; + bool ingress_allowed = false; + + if (readable_socket_) { + // If any of the external sockets got data, see if it will be passed through + r = readable_socket_->recvfrom(buf, maxlen, len, 0, from); + const nr_transport_addr to = readable_socket_->my_addr(); + readable_socket_ = nullptr; + if (!r) { + PortMapping* port_mapping_used; + ingress_allowed = allow_ingress(to, *from, &port_mapping_used); + if (ingress_allowed) { + r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %s received from %s via %s", + internal_socket_->my_addr().as_string, from->as_string, + port_mapping_used->external_socket_->my_addr().as_string); + if (nat_->refresh_on_ingress_) { + port_mapping_used->last_used_ = PR_IntervalNow(); + } + } + } + } else { + // If no external socket has data, see if there's any data that was sent + // directly to the TestNrSocket, and eat it if it isn't supposed to get + // through. + r = internal_socket_->recvfrom(buf, maxlen, len, flags, from); + if (!r) { + // We do not use allow_ingress() here because that only handles traffic + // landing on an external port. + ingress_allowed = (!nat_->enabled_ || nat_->is_an_internal_tuple(*from)); + if (!ingress_allowed) { + r_log(LOG_GENERIC, LOG_INFO, + "TestNrSocket %s denying ingress from %s: " + "Not behind the same NAT", + internal_socket_->my_addr().as_string, from->as_string); + } else { + r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %s received from %s", + internal_socket_->my_addr().as_string, from->as_string); + } + } + } + + // Kinda bad that we are forced to give the app a readable callback and then + // say "Oh, never mind...", but the alternative is to totally decouple the + // callbacks from STS and the callbacks the app sets. On the bright side, this + // speeds up unit tests where we are verifying that ingress is forbidden, + // since they'll get a readable callback and then an error, instead of having + // to wait for a timeout. + if (!ingress_allowed) { + *len = 0; + r = R_WOULDBLOCK; + } + + return r; +} + +bool TestNrSocket::allow_ingress(const nr_transport_addr& to, + const nr_transport_addr& from, + PortMapping** port_mapping_used) const { + // This is only called for traffic arriving at a port mapping + MOZ_ASSERT(nat_->enabled_); + MOZ_ASSERT(!nat_->is_an_internal_tuple(from)); + + // Find the port mapping (if any) that this packet landed on + for (PortMapping* port_mapping : port_mappings_) { + if (!nr_transport_addr_cmp(&to, &port_mapping->external_socket_->my_addr(), + NR_TRANSPORT_ADDR_CMP_MODE_ALL)) { + *port_mapping_used = port_mapping; + } + } + + if (NS_WARN_IF(!(*port_mapping_used))) { + MOZ_ASSERT(false); + r_log(LOG_GENERIC, LOG_INFO, + "TestNrSocket %s denying ingress from %s: " + "No port mapping for this local port! What?", + internal_socket_->my_addr().as_string, from.as_string); + return false; + } + + if (!port_mapping_matches(**port_mapping_used, from, nat_->filtering_type_)) { + r_log(LOG_GENERIC, LOG_INFO, + "TestNrSocket %s denying ingress from %s: " + "Filtered (no port mapping for source)", + internal_socket_->my_addr().as_string, from.as_string); + return false; + } + + if (is_port_mapping_stale(**port_mapping_used)) { + r_log(LOG_GENERIC, LOG_INFO, + "TestNrSocket %s denying ingress from %s: " + "Stale port mapping", + internal_socket_->my_addr().as_string, from.as_string); + return false; + } + + if (!nat_->allow_hairpinning_ && nat_->is_my_external_tuple(from)) { + r_log(LOG_GENERIC, LOG_INFO, + "TestNrSocket %s denying ingress from %s: " + "Hairpinning disallowed", + internal_socket_->my_addr().as_string, from.as_string); + return false; + } + + return true; +} + +int TestNrSocket::connect(const nr_transport_addr* addr) { + r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %p %s connecting to %s", this, + internal_socket_->my_addr().as_string, addr->as_string); + + if (connect_invoked_ || !port_mappings_.empty()) { + MOZ_CRASH("TestNrSocket::connect() called more than once!"); + return R_INTERNAL; + } + + if (maybe_get_redirect_targets(addr).isSome()) { + // If we are simulating STUN redirects for |addr|, we need to pretend that + // the TCP connection worked, since |addr| probably does not actually point + // at something that exists. + connect_fake_stun_address_.reset(new nr_transport_addr); + nr_transport_addr_copy(connect_fake_stun_address_.get(), addr); + + // We dispatch this, otherwise nICEr can trip over its shoelaces + GetCurrentSerialEventTarget()->Dispatch( + NS_NewRunnableFunction("Async writeable callback for TestNrSocket", + [this, self = RefPtr<TestNrSocket>(this)] { + if (poll_flags() & PR_POLL_WRITE) { + fire_callback(NR_ASYNC_WAIT_WRITE); + } + })); + + return R_WOULDBLOCK; + } + + if (!nat_->enabled_ || + addr->protocol == IPPROTO_UDP // Horrible hack to allow default address + // discovery to work. Only works because + // we don't normally connect on UDP. + || nat_->is_an_internal_tuple(*addr)) { + // This will set connect_invoked_ + return internal_socket_->connect(addr); + } + + RefPtr<NrSocketBase> external_socket(create_external_socket(*addr)); + if (!external_socket) { + return R_INTERNAL; + } + + PortMapping* port_mapping = create_port_mapping(*addr, external_socket); + port_mappings_.push_back(port_mapping); + int r = port_mapping->external_socket_->connect(addr); + if (r && r != R_WOULDBLOCK) { + return r; + } + + port_mapping->last_used_ = PR_IntervalNow(); + + if (poll_flags() & PR_POLL_READ) { + port_mapping->async_wait(NR_ASYNC_WAIT_READ, + port_mapping_tcp_passthrough_callback, this, + (char*)__FUNCTION__, __LINE__); + } + + return r; +} + +int TestNrSocket::write(const void* msg, size_t len, size_t* written) { + r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %p %s writing", this, + internal_socket_->my_addr().as_string); + + UCHAR* buf = static_cast<UCHAR*>(const_cast<void*>(msg)); + + if (nat_->nat_delegate_ && + nat_->nat_delegate_->on_write(nat_, msg, len, written)) { + return R_INTERNAL; + } + + if (nat_->block_stun_ && nr_is_stun_message(buf, len)) { + // Should cause this socket to be abandoned + r_log(LOG_GENERIC, LOG_DEBUG, + "TestNrSocket %s dropping outgoing TCP " + "because it is configured to drop STUN", + my_addr().as_string); + return R_INTERNAL; + } + + if (nr_is_stun_request_message(buf, len) && connect_fake_stun_address_ && + maybe_send_fake_response(buf, len, connect_fake_stun_address_.get())) { + return 0; + } + + if (nat_->block_tcp_ && !tls_) { + // Should cause this socket to be abandoned + r_log(LOG_GENERIC, LOG_DEBUG, + "TestNrSocket %s dropping outgoing TCP " + "because it is configured to drop TCP", + my_addr().as_string); + return R_INTERNAL; + } + + if (nat_->block_tls_ && tls_) { + // Should cause this socket to be abandoned + r_log(LOG_GENERIC, LOG_DEBUG, + "TestNrSocket %s dropping outgoing TLS " + "because it is configured to drop TLS", + my_addr().as_string); + return R_INTERNAL; + } + + if (port_mappings_.empty()) { + // The no-nat case, just pass call through. + r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %s writing", + my_addr().as_string); + + return internal_socket_->write(msg, len, written); + } + destroy_stale_port_mappings(); + if (port_mappings_.empty()) { + r_log(LOG_GENERIC, LOG_DEBUG, + "TestNrSocket %s dropping outgoing TCP " + "because the port mapping was stale", + my_addr().as_string); + return R_INTERNAL; + } + // This is TCP only + MOZ_ASSERT(port_mappings_.size() == 1); + r_log(LOG_GENERIC, LOG_DEBUG, "PortMapping %s -> %s writing", + port_mappings_.front()->external_socket_->my_addr().as_string, + port_mappings_.front()->remote_address_.as_string); + port_mappings_.front()->last_used_ = PR_IntervalNow(); + return port_mappings_.front()->external_socket_->write(msg, len, written); +} + +int TestNrSocket::read(void* buf, size_t maxlen, size_t* len) { + r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %p %s reading", this, + internal_socket_->my_addr().as_string); + + if (!read_buffer_.empty()) { + r_log(LOG_GENERIC, LOG_DEBUG, + "TestNrSocket %p %s has stuff in read_buffer_", this, + internal_socket_->my_addr().as_string); + UdpPacket packet(std::move(read_buffer_.front())); + read_buffer_.pop_front(); + *len = std::min(maxlen, packet.buffer_->len()); + memcpy(buf, packet.buffer_->data(), *len); + if (*len != packet.buffer_->len()) { + // Put remaining bytes in new packet, at the front. + read_buffer_.emplace_front(packet.buffer_->data() + *len, + packet.buffer_->len() - *len, + packet.remote_address_); + } + return 0; + } + + if (connect_fake_stun_address_) { + return R_WOULDBLOCK; + } + + int r; + + if (port_mappings_.empty()) { + r = internal_socket_->read(buf, maxlen, len); + } else { + MOZ_ASSERT(port_mappings_.size() == 1); + r = port_mappings_.front()->external_socket_->read(buf, maxlen, len); + if (!r && nat_->refresh_on_ingress_) { + port_mappings_.front()->last_used_ = PR_IntervalNow(); + } + } + + if (r) { + return r; + } + + if (nat_->nat_delegate_ && + nat_->nat_delegate_->on_read(nat_, buf, maxlen, len)) { + return R_INTERNAL; + } + + if (nat_->block_tcp_ && !tls_) { + // Should cause this socket to be abandoned + return R_INTERNAL; + } + + if (nat_->block_tls_ && tls_) { + // Should cause this socket to be abandoned + return R_INTERNAL; + } + + UCHAR* cbuf = static_cast<UCHAR*>(const_cast<void*>(buf)); + if (nat_->block_stun_ && nr_is_stun_message(cbuf, *len)) { + // Should cause this socket to be abandoned + return R_INTERNAL; + } + + return r; +} + +int TestNrSocket::async_wait(int how, NR_async_cb cb, void* cb_arg, + char* function, int line) { + r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %s waiting for %s", + internal_socket_->my_addr().as_string, + how == NR_ASYNC_WAIT_READ ? "read" : "write"); + + int r; + + if (how == NR_ASYNC_WAIT_READ) { + NrSocketBase::async_wait(how, cb, cb_arg, function, line); + if (!read_buffer_.empty()) { + fire_readable_callback(); + return 0; + } + + // Make sure we're waiting on the socket for the internal address + r = internal_socket_->async_wait(how, socket_readable_callback, this, + function, line); + } else { + if (connect_fake_stun_address_) { + // Fake TCP connection case; register the callback on this socket, not + // a real one. + return NrSocketBase::async_wait(how, cb, cb_arg, function, line); + } + + // For write, just use the readiness of the internal socket, since we queue + // everything for the port mappings. + r = internal_socket_->async_wait(how, cb, cb_arg, function, line); + } + + if (r) { + r_log(LOG_GENERIC, LOG_ERR, + "TestNrSocket %s failed to async_wait for " + "internal socket: %d\n", + internal_socket_->my_addr().as_string, r); + return r; + } + + if (is_tcp_connection_behind_nat()) { + // Bypass all port-mapping related logic + return 0; + } + + if (internal_socket_->my_addr().protocol == IPPROTO_TCP) { + // For a TCP connection through a simulated NAT, these signals are + // just passed through. + MOZ_ASSERT(port_mappings_.size() == 1); + + return port_mappings_.front()->async_wait( + how, port_mapping_tcp_passthrough_callback, this, function, line); + } + if (how == NR_ASYNC_WAIT_READ) { + // For UDP port mappings, we decouple the writeable callbacks + for (PortMapping* port_mapping : port_mappings_) { + // Be ready to receive traffic on our port mappings + r = port_mapping->async_wait(how, socket_readable_callback, this, + function, line); + if (r) { + r_log(LOG_GENERIC, LOG_ERR, + "TestNrSocket %s failed to async_wait for " + "port mapping: %d\n", + internal_socket_->my_addr().as_string, r); + return r; + } + } + } + + return 0; +} + +void TestNrSocket::cancel_port_mapping_async_wait(int how) { + for (PortMapping* port_mapping : port_mappings_) { + port_mapping->cancel(how); + } +} + +int TestNrSocket::cancel(int how) { + r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %s stop waiting for %s", + internal_socket_->my_addr().as_string, + how == NR_ASYNC_WAIT_READ ? "read" : "write"); + + if (connect_fake_stun_address_) { + return NrSocketBase::cancel(how); + } + + // Writable callbacks are decoupled except for the TCP case + if (how == NR_ASYNC_WAIT_READ || + internal_socket_->my_addr().protocol == IPPROTO_TCP) { + cancel_port_mapping_async_wait(how); + } + + return internal_socket_->cancel(how); +} + +bool TestNrSocket::has_port_mappings() const { return !port_mappings_.empty(); } + +bool TestNrSocket::is_my_external_tuple(const nr_transport_addr& addr) const { + for (PortMapping* port_mapping : port_mappings_) { + nr_transport_addr port_mapping_addr; + if (port_mapping->external_socket_->getaddr(&port_mapping_addr)) { + MOZ_CRASH("NrSocket::getaddr failed!"); + } + + if (!nr_transport_addr_cmp(&addr, &port_mapping_addr, + NR_TRANSPORT_ADDR_CMP_MODE_ALL)) { + return true; + } + } + return false; +} + +bool TestNrSocket::is_port_mapping_stale( + const PortMapping& port_mapping) const { + PRIntervalTime now = PR_IntervalNow(); + PRIntervalTime elapsed_ticks = now - port_mapping.last_used_; + uint32_t idle_duration = PR_IntervalToMilliseconds(elapsed_ticks); + return idle_duration > nat_->mapping_timeout_; +} + +void TestNrSocket::destroy_stale_port_mappings() { + for (auto i = port_mappings_.begin(); i != port_mappings_.end();) { + auto temp = i; + ++i; + if (is_port_mapping_stale(**temp)) { + r_log(LOG_GENERIC, LOG_INFO, + "TestNrSocket %s destroying port mapping %s -> %s", + internal_socket_->my_addr().as_string, + (*temp)->external_socket_->my_addr().as_string, + (*temp)->remote_address_.as_string); + + port_mappings_.erase(temp); + } + } +} + +void TestNrSocket::socket_readable_callback(void* real_sock_v, int how, + void* test_sock_v) { + TestNrSocket* test_socket = static_cast<TestNrSocket*>(test_sock_v); + NrSocketBase* real_socket = static_cast<NrSocketBase*>(real_sock_v); + + test_socket->on_socket_readable(real_socket); +} + +void TestNrSocket::on_socket_readable(NrSocketBase* real_socket) { + if (!readable_socket_ && (real_socket != internal_socket_)) { + readable_socket_ = real_socket; + } + + fire_readable_callback(); +} + +void TestNrSocket::fire_readable_callback() { + MOZ_ASSERT(poll_flags() & PR_POLL_READ); + r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %p %s ready for read", this, + internal_socket_->my_addr().as_string); + fire_callback(NR_ASYNC_WAIT_READ); +} + +void TestNrSocket::port_mapping_writeable_callback(void* ext_sock_v, int how, + void* test_sock_v) { + TestNrSocket* test_socket = static_cast<TestNrSocket*>(test_sock_v); + NrSocketBase* external_socket = static_cast<NrSocketBase*>(ext_sock_v); + + test_socket->write_to_port_mapping(external_socket); +} + +void TestNrSocket::write_to_port_mapping(NrSocketBase* external_socket) { + MOZ_ASSERT(internal_socket_->my_addr().protocol != IPPROTO_TCP); + + int r = 0; + for (PortMapping* port_mapping : port_mappings_) { + if (port_mapping->external_socket_ == external_socket) { + // If the send succeeds, or if there was nothing to send, we keep going + r = port_mapping->send_from_queue(); + if (r) { + break; + } + } + } + + if (r == R_WOULDBLOCK) { + // Re-register for writeable callbacks, since we still have stuff to send + NR_ASYNC_WAIT(external_socket, NR_ASYNC_WAIT_WRITE, + &TestNrSocket::port_mapping_writeable_callback, this); + } +} + +void TestNrSocket::port_mapping_tcp_passthrough_callback(void* ext_sock_v, + int how, + void* test_sock_v) { + TestNrSocket* test_socket = static_cast<TestNrSocket*>(test_sock_v); + r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %s firing %s callback", + test_socket->internal_socket_->my_addr().as_string, + how == NR_ASYNC_WAIT_READ ? "readable" : "writeable"); + + test_socket->internal_socket_->fire_callback(how); +} + +bool TestNrSocket::is_tcp_connection_behind_nat() const { + return internal_socket_->my_addr().protocol == IPPROTO_TCP && + port_mappings_.empty(); +} + +TestNrSocket::PortMapping* TestNrSocket::get_port_mapping( + const nr_transport_addr& remote_address, + TestNat::NatBehavior filter) const { + for (PortMapping* port_mapping : port_mappings_) { + if (port_mapping_matches(*port_mapping, remote_address, filter)) { + return port_mapping; + } + } + return nullptr; +} + +/* static */ +bool TestNrSocket::port_mapping_matches(const PortMapping& port_mapping, + const nr_transport_addr& remote_addr, + TestNat::NatBehavior filter) { + int compare_flags; + switch (filter) { + case TestNat::ENDPOINT_INDEPENDENT: + compare_flags = NR_TRANSPORT_ADDR_CMP_MODE_PROTOCOL; + break; + case TestNat::ADDRESS_DEPENDENT: + compare_flags = NR_TRANSPORT_ADDR_CMP_MODE_ADDR; + break; + case TestNat::PORT_DEPENDENT: + compare_flags = NR_TRANSPORT_ADDR_CMP_MODE_ALL; + break; + } + + return !nr_transport_addr_cmp(&remote_addr, &port_mapping.remote_address_, + compare_flags); +} + +TestNrSocket::PortMapping* TestNrSocket::create_port_mapping( + const nr_transport_addr& remote_address, + const RefPtr<NrSocketBase>& external_socket) const { + r_log(LOG_GENERIC, LOG_INFO, "TestNrSocket %s creating port mapping %s -> %s", + internal_socket_->my_addr().as_string, + external_socket->my_addr().as_string, remote_address.as_string); + + return new PortMapping(remote_address, external_socket); +} + +TestNrSocket::PortMapping::PortMapping( + const nr_transport_addr& remote_address, + const RefPtr<NrSocketBase>& external_socket) + : external_socket_(external_socket) { + nr_transport_addr_copy(&remote_address_, &remote_address); +} + +int TestNrSocket::PortMapping::send_from_queue() { + MOZ_ASSERT(remote_address_.protocol != IPPROTO_TCP); + int r = 0; + + while (!send_queue_.empty()) { + UdpPacket& packet = send_queue_.front(); + r_log(LOG_GENERIC, LOG_DEBUG, + "PortMapping %s -> %s sending from queue to %s", + external_socket_->my_addr().as_string, remote_address_.as_string, + packet.remote_address_.as_string); + + r = external_socket_->sendto(packet.buffer_->data(), packet.buffer_->len(), + 0, &packet.remote_address_); + + if (r) { + if (r != R_WOULDBLOCK) { + r_log(LOG_GENERIC, LOG_ERR, "%s: Fatal error %d, stop trying", + __FUNCTION__, r); + send_queue_.clear(); + } else { + r_log(LOG_GENERIC, LOG_DEBUG, "Would block, will retry later"); + } + break; + } + + send_queue_.pop_front(); + } + + return r; +} + +int TestNrSocket::PortMapping::sendto(const void* msg, size_t len, + const nr_transport_addr& to) { + MOZ_ASSERT(remote_address_.protocol != IPPROTO_TCP); + r_log(LOG_GENERIC, LOG_DEBUG, "PortMapping %s -> %s sending to %s", + external_socket_->my_addr().as_string, remote_address_.as_string, + to.as_string); + + last_used_ = PR_IntervalNow(); + int r = external_socket_->sendto(msg, len, 0, &to); + + if (r == R_WOULDBLOCK) { + r_log(LOG_GENERIC, LOG_DEBUG, "Enqueueing UDP packet to %s", to.as_string); + send_queue_.emplace_back(msg, len, to); + return 0; + } + if (r) { + r_log(LOG_GENERIC, LOG_ERR, "Error: %d", r); + } + + return r; +} + +int TestNrSocket::PortMapping::async_wait(int how, NR_async_cb cb, void* cb_arg, + char* function, int line) { + r_log(LOG_GENERIC, LOG_DEBUG, "PortMapping %s -> %s waiting for %s", + external_socket_->my_addr().as_string, remote_address_.as_string, + how == NR_ASYNC_WAIT_READ ? "read" : "write"); + + return external_socket_->async_wait(how, cb, cb_arg, function, line); +} + +int TestNrSocket::PortMapping::cancel(int how) { + r_log(LOG_GENERIC, LOG_DEBUG, "PortMapping %s -> %s stop waiting for %s", + external_socket_->my_addr().as_string, remote_address_.as_string, + how == NR_ASYNC_WAIT_READ ? "read" : "write"); + + return external_socket_->cancel(how); +} + +class nr_stun_message_deleter { + public: + nr_stun_message_deleter() = default; + void operator()(nr_stun_message* msg) const { nr_stun_message_destroy(&msg); } +}; + +bool TestNrSocket::maybe_send_fake_response(const void* msg, size_t len, + const nr_transport_addr* to) { + Maybe<nsTArray<nsCString>> redirect_targets = maybe_get_redirect_targets(to); + if (!redirect_targets.isSome()) { + return false; + } + + std::unique_ptr<nr_stun_message, nr_stun_message_deleter> request; + { + nr_stun_message* temp = nullptr; + if (NS_WARN_IF(nr_stun_message_create2(&temp, (unsigned char*)msg, len))) { + return false; + } + request.reset(temp); + } + + if (NS_WARN_IF(nr_stun_decode_message(request.get(), nullptr, nullptr))) { + return false; + } + + std::unique_ptr<nr_stun_message, nr_stun_message_deleter> response; + { + nr_stun_message* temp = nullptr; + if (nr_stun_message_create(&temp)) { + MOZ_CRASH("nr_stun_message_create failed!"); + } + response.reset(temp); + } + + nr_stun_form_error_response(request.get(), response.get(), 300, + (char*)"Try alternate"); + + int port = 0; + if (nr_transport_addr_get_port(to, &port)) { + MOZ_CRASH(); + } + + for (const nsCString& address : *redirect_targets) { + r_log(LOG_GENERIC, LOG_DEBUG, + "TestNrSocket attempting to add alternate server %s", address.Data()); + nr_transport_addr addr; + if (NS_WARN_IF(nr_str_port_to_transport_addr(address.Data(), port, + IPPROTO_UDP, &addr))) { + continue; + } + if (nr_stun_message_add_alternate_server_attribute(response.get(), &addr)) { + MOZ_CRASH("nr_stun_message_add_alternate_server_attribute failed!"); + } + } + + if (nr_stun_encode_message(response.get())) { + MOZ_CRASH("nr_stun_encode_message failed!"); + } + + nr_transport_addr response_from; + if (nr_transport_addr_is_wildcard(to)) { + // |to| points to an FQDN, and nICEr is delegating DNS lookup to us; we + // aren't _actually_ going to do that though, so we select a bogus address + // for the response to come from. TEST-NET is a fairly reasonable thing to + // use for this. + int port = 0; + if (nr_transport_addr_get_port(to, &port)) { + MOZ_CRASH(); + } + switch (to->ip_version) { + case NR_IPV4: + if (nr_str_port_to_transport_addr("198.51.100.1", port, to->protocol, + &response_from)) { + MOZ_CRASH(); + } + break; + case NR_IPV6: + if (nr_str_port_to_transport_addr("::ffff:198.51.100.1", port, + to->protocol, &response_from)) { + MOZ_CRASH(); + } + break; + default: + MOZ_CRASH(); + } + } else { + nr_transport_addr_copy(&response_from, to); + } + + read_buffer_.emplace_back(response->buffer, response->length, response_from); + + // We dispatch this, otherwise nICEr can trip over its shoelaces + r_log(LOG_GENERIC, LOG_DEBUG, + "TestNrSocket %p scheduling callback for redirect response", this); + GetCurrentSerialEventTarget()->Dispatch(NS_NewRunnableFunction( + "Async readable callback for TestNrSocket", + [this, self = RefPtr<TestNrSocket>(this)] { + if (poll_flags() & PR_POLL_READ) { + fire_readable_callback(); + } else { + r_log(LOG_GENERIC, LOG_DEBUG, + "TestNrSocket %p deferring callback for redirect response", + this); + } + })); + + return true; +} + +Maybe<nsTArray<nsCString>> TestNrSocket::maybe_get_redirect_targets( + const nr_transport_addr* to) const { + Maybe<nsTArray<nsCString>> result; + + // 256 is overkill, but it hardly matters + char addrstring[256]; + if (nr_transport_addr_get_addrstring(to, addrstring, 256)) { + MOZ_CRASH("nr_transport_addr_get_addrstring failed!"); + } + + r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket checking redirect rules for %s", + addrstring); + auto it = nat_->stun_redirect_map_.find(nsCString(addrstring)); + if (it != nat_->stun_redirect_map_.end()) { + result = Some(it->second); + } + + return result; +} + +} // namespace mozilla |