diff --git a/p2p/base/dtls_transport.cc b/p2p/base/dtls_transport.cc index 6f30c6dbdd..7547863bd6 100644 --- a/p2p/base/dtls_transport.cc +++ b/p2p/base/dtls_transport.cc @@ -727,7 +727,7 @@ void DtlsTransport::OnDtlsEvent(rtc::StreamInterface* dtls, int sig, int err) { NotifyPacketReceived(rtc::ReceivedPacket( rtc::MakeArrayView(buf, read), rtc::SocketAddress(), webrtc::Timestamp::Micros(rtc::TimeMicros()), - rtc::ReceivedPacket::kDtlsDecrypted)); + rtc::EcnMarking::kNotEct, rtc::ReceivedPacket::kDtlsDecrypted)); } else if (ret == rtc::SR_EOS) { // Remote peer shut down the association with no error. RTC_LOG(LS_INFO) << ToString() << ": DTLS transport closed by remote"; diff --git a/p2p/base/packet_transport_internal_unittest.cc b/p2p/base/packet_transport_internal_unittest.cc index 1c9df852d1..b8e589a852 100644 --- a/p2p/base/packet_transport_internal_unittest.cc +++ b/p2p/base/packet_transport_internal_unittest.cc @@ -34,9 +34,9 @@ TEST(PacketTransportInternal, EXPECT_EQ(packet.decryption_info(), rtc::ReceivedPacket::kDtlsDecrypted); }); - packet_transport.NotifyPacketReceived( - rtc::ReceivedPacket({}, rtc::SocketAddress(), absl::nullopt, - rtc::ReceivedPacket::kDtlsDecrypted)); + packet_transport.NotifyPacketReceived(rtc::ReceivedPacket( + {}, rtc::SocketAddress(), absl::nullopt, rtc::EcnMarking::kNotEct, + rtc::ReceivedPacket::kDtlsDecrypted)); packet_transport.DeregisterReceivedPacketCallback(&receiver); } diff --git a/rtc_base/BUILD.gn b/rtc_base/BUILD.gn index 775b9100e4..829f0970d1 100644 --- a/rtc_base/BUILD.gn +++ b/rtc_base/BUILD.gn @@ -1046,6 +1046,7 @@ rtc_library("threading") { "../api/task_queue:pending_task_safety_flag", "../api/units:time_delta", "../system_wrappers:field_trial", + "./network:ecn_marking", "synchronization:mutex", "system:no_unique_address", "system:rtc_export", @@ -1092,6 +1093,7 @@ rtc_library("socket") { ":macromagic", ":socket_address", "../api/units:timestamp", + "./network:ecn_marking", "system:rtc_export", "third_party/sigslot", ] diff --git a/rtc_base/async_udp_socket.cc b/rtc_base/async_udp_socket.cc index 3d258bcb26..7f1301dea5 100644 --- a/rtc_base/async_udp_socket.cc +++ b/rtc_base/async_udp_socket.cc @@ -145,9 +145,9 @@ void AsyncUDPSocket::OnReadEvent(Socket* socket) { } *receive_buffer.arrival_time += *socket_time_offset_; } - NotifyPacketReceived(ReceivedPacket(receive_buffer.payload, - receive_buffer.source_address, - receive_buffer.arrival_time)); + NotifyPacketReceived( + ReceivedPacket(receive_buffer.payload, receive_buffer.source_address, + receive_buffer.arrival_time, receive_buffer.ecn)); } void AsyncUDPSocket::OnWriteEvent(Socket* socket) { diff --git a/rtc_base/network/BUILD.gn b/rtc_base/network/BUILD.gn index 2be484e1e0..e2c6fc3c16 100644 --- a/rtc_base/network/BUILD.gn +++ b/rtc_base/network/BUILD.gn @@ -17,6 +17,11 @@ rtc_library("sent_packet") { absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ] } +rtc_source_set("ecn_marking") { + visibility = [ "*" ] + sources = [ "ecn_marking.h" ] +} + rtc_library("received_packet") { visibility = [ "*" ] sources = [ @@ -24,6 +29,7 @@ rtc_library("received_packet") { "received_packet.h", ] deps = [ + ":ecn_marking", "..:socket_address", "../../api:array_view", "../../api/units:timestamp", diff --git a/rtc_base/network/ecn_marking.h b/rtc_base/network/ecn_marking.h new file mode 100644 index 0000000000..e186080a2d --- /dev/null +++ b/rtc_base/network/ecn_marking.h @@ -0,0 +1,42 @@ +/* + * Copyright 2024 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef RTC_BASE_NETWORK_ECN_MARKING_H_ +#define RTC_BASE_NETWORK_ECN_MARKING_H_ + +namespace rtc { + +// TODO(https://bugs.webrtc.org/15368): L4S support is slowly being developed. +// Help is appreciated. + +// L4S Explicit Congestion Notification (ECN) . +// https://www.rfc-editor.org/rfc/rfc9331.html ECT stands for ECN-Capable +// Transport and CE stands for Congestion Experienced. + +// RFC-3168, Section 5 +// +-----+-----+ +// | ECN FIELD | +// +-----+-----+ +// ECT CE [Obsolete] RFC 2481 names for the ECN bits. +// 0 0 Not-ECT +// 0 1 ECT(1) +// 1 0 ECT(0) +// 1 1 CE + +enum class EcnMarking { + kNotEct = 0, // Not ECN-Capable Transport + kEct1 = 1, // ECN-Capable Transport + kEct0 = 2, // Not used by L4s (or webrtc.) + kCe = 3, // Congestion experienced +}; + +} // namespace rtc + +#endif // RTC_BASE_NETWORK_ECN_MARKING_H_ diff --git a/rtc_base/network/received_packet.cc b/rtc_base/network/received_packet.cc index bf8a07ca89..9588e370d2 100644 --- a/rtc_base/network/received_packet.cc +++ b/rtc_base/network/received_packet.cc @@ -20,15 +20,17 @@ namespace rtc { ReceivedPacket::ReceivedPacket(rtc::ArrayView payload, const SocketAddress& source_address, absl::optional arrival_time, + EcnMarking ecn, DecryptionInfo decryption) : payload_(payload), arrival_time_(std::move(arrival_time)), source_address_(source_address), + ecn_(ecn), decryption_info_(decryption) {} ReceivedPacket ReceivedPacket::CopyAndSet( DecryptionInfo decryption_info) const { - return ReceivedPacket(payload_, source_address_, arrival_time_, + return ReceivedPacket(payload_, source_address_, arrival_time_, ecn_, decryption_info); } diff --git a/rtc_base/network/received_packet.h b/rtc_base/network/received_packet.h index b5a6092d89..68dd33b1d1 100644 --- a/rtc_base/network/received_packet.h +++ b/rtc_base/network/received_packet.h @@ -15,6 +15,7 @@ #include "absl/types/optional.h" #include "api/array_view.h" #include "api/units/timestamp.h" +#include "rtc_base/network/ecn_marking.h" #include "rtc_base/socket_address.h" #include "rtc_base/system/rtc_export.h" @@ -38,6 +39,7 @@ class RTC_EXPORT ReceivedPacket { ReceivedPacket(rtc::ArrayView payload, const SocketAddress& source_address, absl::optional arrival_time = absl::nullopt, + EcnMarking ecn = EcnMarking::kNotEct, DecryptionInfo decryption = kNotDecrypted); ReceivedPacket CopyAndSet(DecryptionInfo decryption_info) const; @@ -52,6 +54,9 @@ class RTC_EXPORT ReceivedPacket { return arrival_time_; } + // L4S Explicit Congestion Notification. + EcnMarking ecn() const { return ecn_; } + const DecryptionInfo& decryption_info() const { return decryption_info_; } static ReceivedPacket CreateFromLegacy( @@ -73,6 +78,7 @@ class RTC_EXPORT ReceivedPacket { rtc::ArrayView payload_; absl::optional arrival_time_; const SocketAddress& source_address_; + EcnMarking ecn_; DecryptionInfo decryption_info_; }; diff --git a/rtc_base/physical_socket_server.cc b/rtc_base/physical_socket_server.cc index c3bc1814a1..447e3da2ef 100644 --- a/rtc_base/physical_socket_server.cc +++ b/rtc_base/physical_socket_server.cc @@ -48,6 +48,7 @@ #include "rtc_base/event.h" #include "rtc_base/ip_address.h" #include "rtc_base/logging.h" +#include "rtc_base/network/ecn_marking.h" #include "rtc_base/network_monitor.h" #include "rtc_base/synchronization/mutex.h" #include "rtc_base/time_utils.h" @@ -108,6 +109,33 @@ typedef char* SockOptArg; #endif namespace { + +// RFC-3168, Section 5. ECN is the two least significant bits. +static constexpr uint8_t kEcnMask = 0x03; + +#if defined(WEBRTC_POSIX) + +rtc::EcnMarking EcnFromDs(uint8_t ds) { + // RFC-3168, Section 5. + constexpr uint8_t ECN_ECT1 = 0x01; + constexpr uint8_t ECN_ECT0 = 0x02; + constexpr uint8_t ECN_CE = 0x03; + const uint8_t ecn = ds & kEcnMask; + + if (ecn == ECN_ECT1) { + return rtc::EcnMarking::kEct1; + } + if (ecn == ECN_ECT0) { + return rtc::EcnMarking::kEct0; + } + if (ecn == ECN_CE) { + return rtc::EcnMarking::kCe; + } + return rtc::EcnMarking::kNotEct; +} + +#endif + class ScopedSetTrue { public: ScopedSetTrue(bool* value) : value_(value) { @@ -125,6 +153,7 @@ class ScopedSetTrue { bool IsScmTimeStampExperimentDisabled() { return webrtc::field_trial::IsDisabled("WebRTC-SCM-Timestamp"); } + } // namespace namespace rtc { @@ -314,8 +343,19 @@ int PhysicalSocket::GetOption(Option opt, int* value) { #if defined(WEBRTC_POSIX) // unshift DSCP value to get six most significant bits of IP DiffServ field *value >>= 2; +#endif + } else if (opt == OPT_SEND_ECN) { +#if defined(WEBRTC_POSIX) + // Least 2 significant bits. + *value = *value & kEcnMask; +#endif + } else if (opt == OPT_RECV_ECN) { +#if defined(WEBRTC_POSIX) + // Least 2 significant bits. + *value = *value & kEcnMask; #endif } + return ret; } @@ -329,10 +369,13 @@ int PhysicalSocket::SetOption(Option opt, int value) { value = (value) ? IP_PMTUDISC_DO : IP_PMTUDISC_DONT; #endif } else if (opt == OPT_DSCP) { -#if defined(WEBRTC_POSIX) - // shift DSCP value to fit six most significant bits of IP DiffServ field - value <<= 2; -#endif + // IP DiffServ consists of DSCP 6 most significant, ECN 2 least + // significant. + dscp_ = value << 2; + value = dscp_ + (ecn_ & kEcnMask); + } else if (opt == OPT_SEND_ECN) { + ecn_ = value; + value = dscp_ + (ecn_ & kEcnMask); } #if defined(WEBRTC_POSIX) if (sopt == IPV6_TCLASS) { @@ -401,8 +444,8 @@ int PhysicalSocket::SendTo(const void* buffer, } int PhysicalSocket::Recv(void* buffer, size_t length, int64_t* timestamp) { - int received = - DoReadFromSocket(buffer, length, /*out_addr*/ nullptr, timestamp); + int received = DoReadFromSocket(buffer, length, /*out_addr*/ nullptr, + timestamp, /*ecn=*/nullptr); if ((received == 0) && (length != 0)) { // Note: on graceful shutdown, recv can return 0. In this case, we // pretend it is blocking, and then signal close, so that simplifying @@ -431,7 +474,7 @@ int PhysicalSocket::RecvFrom(void* buffer, size_t length, SocketAddress* out_addr, int64_t* timestamp) { - int received = DoReadFromSocket(buffer, length, out_addr, timestamp); + int received = DoReadFromSocket(buffer, length, out_addr, timestamp, nullptr); UpdateLastError(); int error = GetError(); @@ -450,9 +493,9 @@ int PhysicalSocket::RecvFrom(ReceiveBuffer& buffer) { static constexpr int BUF_SIZE = 64 * 1024; buffer.payload.EnsureCapacity(BUF_SIZE); - int received = - DoReadFromSocket(buffer.payload.data(), buffer.payload.capacity(), - &buffer.source_address, ×tamp); + int received = DoReadFromSocket( + buffer.payload.data(), buffer.payload.capacity(), &buffer.source_address, + ×tamp, ecn_ ? &buffer.ecn : nullptr); buffer.payload.SetSize(received > 0 ? received : 0); if (received > 0 && timestamp != -1) { buffer.arrival_time = webrtc::Timestamp::Micros(timestamp); @@ -472,7 +515,8 @@ int PhysicalSocket::RecvFrom(ReceiveBuffer& buffer) { int PhysicalSocket::DoReadFromSocket(void* buffer, size_t length, SocketAddress* out_addr, - int64_t* timestamp) { + int64_t* timestamp, + EcnMarking* ecn) { sockaddr_storage addr_storage; socklen_t addr_len = sizeof(addr_storage); sockaddr* addr = reinterpret_cast(&addr_storage); @@ -487,8 +531,10 @@ int PhysicalSocket::DoReadFromSocket(void* buffer, msg.msg_name = addr; msg.msg_namelen = addr_len; } - char control[CMSG_SPACE(sizeof(struct timeval))] = {}; - if (timestamp) { + // TODO(bugs.webrtc.org/15368): What size is needed? IPV6_TCLASS is supposed + // to be an int. Why is a larger size needed? + char control[CMSG_SPACE(sizeof(struct timeval) + 5 * sizeof(int))] = {}; + if (timestamp || ecn) { *timestamp = -1; msg.msg_control = &control; msg.msg_controllen = sizeof(control); @@ -498,17 +544,23 @@ int PhysicalSocket::DoReadFromSocket(void* buffer, // An error occured or shut down. return received; } - if (timestamp) { + if (timestamp || ecn) { struct cmsghdr* cmsg; for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) { + if (ecn) { + if ((cmsg->cmsg_type == IPV6_TCLASS && + cmsg->cmsg_level == IPPROTO_IPV6) || + (cmsg->cmsg_type == IP_TOS && cmsg->cmsg_level == IPPROTO_IP)) { + *ecn = EcnFromDs(CMSG_DATA(cmsg)[0]); + } + } if (cmsg->cmsg_level != SOL_SOCKET) continue; - if (cmsg->cmsg_type == SCM_TIMESTAMP) { + if (timestamp && cmsg->cmsg_type == SCM_TIMESTAMP) { timeval* ts = reinterpret_cast(CMSG_DATA(cmsg)); *timestamp = rtc::kNumMicrosecsPerSec * static_cast(ts->tv_sec) + static_cast(ts->tv_usec); - break; } } } @@ -699,6 +751,34 @@ int PhysicalSocket::TranslateOption(Option opt, int* slevel, int* sopt) { #else RTC_LOG(LS_WARNING) << "Socket::OPT_DSCP not supported."; return -1; +#endif + case OPT_SEND_ECN: +#if defined(WEBRTC_POSIX) + if (family_ == AF_INET6) { + *slevel = IPPROTO_IPV6; + *sopt = IPV6_TCLASS; + } else { + *slevel = IPPROTO_IP; + *sopt = IP_TOS; + } + break; +#else + RTC_LOG(LS_WARNING) << "Socket::OPT_SEND_ESN not supported."; + return -1; +#endif + case OPT_RECV_ECN: +#if defined(WEBRTC_POSIX) + if (family_ == AF_INET6) { + *slevel = IPPROTO_IPV6; + *sopt = IPV6_RECVTCLASS; + } else { + *slevel = IPPROTO_IP; + *sopt = IP_RECVTOS; + } + break; +#else + RTC_LOG(LS_WARNING) << "Socket::OPT_RECV_ECN not supported."; + return -1; #endif case OPT_RTP_SENDTIME_EXTN_ID: return -1; // No logging is necessary as this not a OS socket option. diff --git a/rtc_base/physical_socket_server.h b/rtc_base/physical_socket_server.h index 2af563a3ca..ec30281e90 100644 --- a/rtc_base/physical_socket_server.h +++ b/rtc_base/physical_socket_server.h @@ -224,7 +224,8 @@ class PhysicalSocket : public Socket, public sigslot::has_slots<> { int DoReadFromSocket(void* buffer, size_t length, SocketAddress* out_addr, - int64_t* timestamp); + int64_t* timestamp, + EcnMarking* ecn); void OnResolveResult(const webrtc::AsyncDnsResolverResult& resolver); @@ -246,6 +247,8 @@ class PhysicalSocket : public Socket, public sigslot::has_slots<> { int error_ RTC_GUARDED_BY(mutex_); ConnState state_; std::unique_ptr resolver_; + uint8_t dscp_ = 0; // 6bit. + uint8_t ecn_ = 0; // 2bits. #if !defined(NDEBUG) std::string dbg_addr_; diff --git a/rtc_base/physical_socket_server_unittest.cc b/rtc_base/physical_socket_server_unittest.cc index de64a31812..ccb548d2f3 100644 --- a/rtc_base/physical_socket_server_unittest.cc +++ b/rtc_base/physical_socket_server_unittest.cc @@ -484,6 +484,19 @@ TEST_F(PhysicalSocketTest, TestSocketRecvTimestampIPv6ScmExperimentDisabled) { } #endif +#if !defined(WEBRTC_MAC) && !defined(WEBRTC_IOS) +// TODO(bugs.webrtc.org/15368): IpV4 fails on IOS and MAC. IPV6 works. +TEST_F(PhysicalSocketTest, TestSocketSendRecvWithEcnIPv4) { + MAYBE_SKIP_IPV6; + SocketTest::TestSocketSendRecvWithEcnIPV4(); +} +#endif + +TEST_F(PhysicalSocketTest, TestSocketSendRecvWithEcnIPv6) { + MAYBE_SKIP_IPV6; + SocketTest::TestSocketSendRecvWithEcnIPV6(); +} + // Verify that if the socket was unable to be bound to a real network interface // (not loopback), Bind will return an error. TEST_F(PhysicalSocketTest, diff --git a/rtc_base/socket.h b/rtc_base/socket.h index 98e468e754..4e35406798 100644 --- a/rtc_base/socket.h +++ b/rtc_base/socket.h @@ -30,6 +30,7 @@ #include "api/units/timestamp.h" #include "rtc_base/buffer.h" +#include "rtc_base/network/ecn_marking.h" #include "rtc_base/socket_address.h" #include "rtc_base/system/rtc_export.h" #include "rtc_base/third_party/sigslot/sigslot.h" @@ -91,6 +92,7 @@ class RTC_EXPORT Socket { absl::optional arrival_time; SocketAddress source_address; + EcnMarking ecn = EcnMarking::kNotEct; Buffer& payload; }; virtual ~Socket() {} @@ -144,6 +146,8 @@ class RTC_EXPORT Socket { OPT_RTP_SENDTIME_EXTN_ID, // This is a non-traditional socket option param. // This is specific to libjingle and will be used // if SendTime option is needed at socket level. + OPT_SEND_ECN, // 2-bit ECN + OPT_RECV_ECN }; virtual int GetOption(Option opt, int* value) = 0; virtual int SetOption(Option opt, int value) = 0; diff --git a/rtc_base/socket_unittest.cc b/rtc_base/socket_unittest.cc index 5314128d0a..c5e791b293 100644 --- a/rtc_base/socket_unittest.cc +++ b/rtc_base/socket_unittest.cc @@ -234,6 +234,15 @@ void SocketTest::TestUdpSocketRecvTimestampUseRtcEpochIPv6() { UdpSocketRecvTimestampUseRtcEpoch(kIPv6Loopback); } +void SocketTest::TestSocketSendRecvWithEcnIPV4() { + SocketSendRecvWithEcn(kIPv4Loopback); +} + +void SocketTest::TestSocketSendRecvWithEcnIPV6() { + MAYBE_SKIP_IPV6; + SocketSendRecvWithEcn(kIPv6Loopback); +} + // For unbound sockets, GetLocalAddress / GetRemoteAddress return AF_UNSPEC // values on Windows, but an empty address of the same family on Linux/MacOS X. bool IsUnspecOrEmptyIP(const IPAddress& address) { @@ -1079,6 +1088,18 @@ void SocketTest::GetSetOptionsInternal(const IPAddress& loopback) { ASSERT_NE(-1, socket->SetOption(Socket::OPT_DSCP, desired_dscp)); ASSERT_NE(-1, socket->GetOption(Socket::OPT_DSCP, ¤t_dscp)); ASSERT_EQ(desired_dscp, current_dscp); + + int current_send_esn, desired_send_esn = 1; + ASSERT_NE(-1, socket->GetOption(Socket::OPT_SEND_ECN, ¤t_send_esn)); + ASSERT_NE(-1, socket->SetOption(Socket::OPT_SEND_ECN, desired_send_esn)); + ASSERT_NE(-1, socket->GetOption(Socket::OPT_SEND_ECN, ¤t_send_esn)); + ASSERT_EQ(current_send_esn, desired_send_esn); + + int current_recv_esn, desired_recv_esn = 1; + ASSERT_NE(-1, socket->GetOption(Socket::OPT_RECV_ECN, ¤t_recv_esn)); + ASSERT_NE(-1, socket->SetOption(Socket::OPT_RECV_ECN, desired_recv_esn)); + ASSERT_NE(-1, socket->GetOption(Socket::OPT_RECV_ECN, ¤t_recv_esn)); + ASSERT_EQ(current_recv_esn, desired_recv_esn); #endif } @@ -1143,4 +1164,41 @@ void SocketTest::UdpSocketRecvTimestampUseRtcEpoch(const IPAddress& loopback) { EXPECT_GT(packet_2->packet_time->us(), packet_1->packet_time->us()); EXPECT_NEAR(packet_2->packet_time->us(), rtc::TimeMicros(), 1000'000); } + +void SocketTest::SocketSendRecvWithEcn(const IPAddress& loopback) { + StreamSink sink; + std::unique_ptr socket( + socket_factory_->CreateSocket(loopback.family(), SOCK_DGRAM)); + EXPECT_EQ(0, socket->Bind(SocketAddress(loopback, 0))); + SocketAddress address = socket->GetLocalAddress(); + sink.Monitor(socket.get()); + rtc::Buffer buffer; + Socket::ReceiveBuffer receive_buffer(buffer); + + socket->SendTo("foo", 3, address); + EXPECT_TRUE_WAIT(sink.Check(socket.get(), SSE_READ), kTimeout); + ASSERT_GT(socket->RecvFrom(receive_buffer), 0); + EXPECT_EQ(receive_buffer.ecn, EcnMarking::kNotEct); + + socket->SetOption(Socket::OPT_SEND_ECN, 1); // Ect(1) + socket->SetOption(Socket::OPT_RECV_ECN, 1); + + socket->SendTo("bar", 3, address); + EXPECT_TRUE_WAIT(sink.Check(socket.get(), SSE_READ), kTimeout); + ASSERT_GT(socket->RecvFrom(receive_buffer), 0); + EXPECT_EQ(receive_buffer.ecn, EcnMarking::kEct1); + + socket->SetOption(Socket::OPT_SEND_ECN, 2); // Ect(0) + socket->SendTo("bar", 3, address); + EXPECT_TRUE_WAIT(sink.Check(socket.get(), SSE_READ), kTimeout); + ASSERT_GT(socket->RecvFrom(receive_buffer), 0); + EXPECT_EQ(receive_buffer.ecn, EcnMarking::kEct0); + + socket->SetOption(Socket::OPT_SEND_ECN, 3); // Ce + socket->SendTo("bar", 3, address); + EXPECT_TRUE_WAIT(sink.Check(socket.get(), SSE_READ), kTimeout); + ASSERT_GT(socket->RecvFrom(receive_buffer), 0); + EXPECT_EQ(receive_buffer.ecn, EcnMarking::kCe); +} + } // namespace rtc diff --git a/rtc_base/socket_unittest.h b/rtc_base/socket_unittest.h index db79be2eb8..4d24f3641d 100644 --- a/rtc_base/socket_unittest.h +++ b/rtc_base/socket_unittest.h @@ -64,6 +64,8 @@ class SocketTest : public ::testing::Test { void TestSocketRecvTimestampIPv6(); void TestUdpSocketRecvTimestampUseRtcEpochIPv4(); void TestUdpSocketRecvTimestampUseRtcEpochIPv6(); + void TestSocketSendRecvWithEcnIPV4(); + void TestSocketSendRecvWithEcnIPV6(); static const int kTimeout = 5000; // ms const IPAddress kIPv4Loopback; @@ -95,6 +97,7 @@ class SocketTest : public ::testing::Test { void GetSetOptionsInternal(const IPAddress& loopback); void SocketRecvTimestamp(const IPAddress& loopback); void UdpSocketRecvTimestampUseRtcEpoch(const IPAddress& loopback); + void SocketSendRecvWithEcn(const IPAddress& loopback); SocketFactory* socket_factory_; };