Generalize EventLoopTcpClientSocket between TCP and connected-UDP

Gives us the socket support needed for CONNECT-UDP.  As far as interface and implementation goes, connected UDP sockets are pretty much the same as TCP sockets other than one small parameter sent to ::socket().  But the expected behavior is obviously very different, so this CL is primarily added testing for UDP socket expectations.

PiperOrigin-RevId: 476185935
diff --git a/build/source_list.bzl b/build/source_list.bzl
index 266e20f..b2f73be 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -949,8 +949,8 @@
     "quic/core/batch_writer/quic_batch_writer_test.h",
     "quic/core/batch_writer/quic_gso_batch_writer.h",
     "quic/core/batch_writer/quic_sendmmsg_batch_writer.h",
+    "quic/core/io/event_loop_connecting_client_socket.h",
     "quic/core/io/event_loop_socket_factory.h",
-    "quic/core/io/event_loop_tcp_client_socket.h",
     "quic/core/io/quic_default_event_loop.h",
     "quic/core/io/quic_event_loop.h",
     "quic/core/io/quic_poll_event_loop.h",
@@ -980,8 +980,8 @@
     "quic/core/batch_writer/quic_batch_writer_buffer.cc",
     "quic/core/batch_writer/quic_gso_batch_writer.cc",
     "quic/core/batch_writer/quic_sendmmsg_batch_writer.cc",
+    "quic/core/io/event_loop_connecting_client_socket.cc",
     "quic/core/io/event_loop_socket_factory.cc",
-    "quic/core/io/event_loop_tcp_client_socket.cc",
     "quic/core/io/quic_default_event_loop.cc",
     "quic/core/io/quic_poll_event_loop.cc",
     "quic/core/io/socket_posix.cc",
@@ -1292,7 +1292,7 @@
     "quic/core/http/quic_spdy_client_session_test.cc",
     "quic/core/http/quic_spdy_client_stream_test.cc",
     "quic/core/http/quic_spdy_server_stream_base_test.cc",
-    "quic/core/io/event_loop_tcp_client_socket_test.cc",
+    "quic/core/io/event_loop_connecting_client_socket_test.cc",
     "quic/core/io/quic_all_event_loops_test.cc",
     "quic/core/io/quic_poll_event_loop_test.cc",
     "quic/core/io/socket_test.cc",
diff --git a/build/source_list.gni b/build/source_list.gni
index 7b04267..18fcd44 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -949,8 +949,8 @@
     "src/quiche/quic/core/batch_writer/quic_batch_writer_test.h",
     "src/quiche/quic/core/batch_writer/quic_gso_batch_writer.h",
     "src/quiche/quic/core/batch_writer/quic_sendmmsg_batch_writer.h",
+    "src/quiche/quic/core/io/event_loop_connecting_client_socket.h",
     "src/quiche/quic/core/io/event_loop_socket_factory.h",
-    "src/quiche/quic/core/io/event_loop_tcp_client_socket.h",
     "src/quiche/quic/core/io/quic_default_event_loop.h",
     "src/quiche/quic/core/io/quic_event_loop.h",
     "src/quiche/quic/core/io/quic_poll_event_loop.h",
@@ -980,8 +980,8 @@
     "src/quiche/quic/core/batch_writer/quic_batch_writer_buffer.cc",
     "src/quiche/quic/core/batch_writer/quic_gso_batch_writer.cc",
     "src/quiche/quic/core/batch_writer/quic_sendmmsg_batch_writer.cc",
+    "src/quiche/quic/core/io/event_loop_connecting_client_socket.cc",
     "src/quiche/quic/core/io/event_loop_socket_factory.cc",
-    "src/quiche/quic/core/io/event_loop_tcp_client_socket.cc",
     "src/quiche/quic/core/io/quic_default_event_loop.cc",
     "src/quiche/quic/core/io/quic_poll_event_loop.cc",
     "src/quiche/quic/core/io/socket_posix.cc",
@@ -1292,7 +1292,7 @@
     "src/quiche/quic/core/http/quic_spdy_client_session_test.cc",
     "src/quiche/quic/core/http/quic_spdy_client_stream_test.cc",
     "src/quiche/quic/core/http/quic_spdy_server_stream_base_test.cc",
-    "src/quiche/quic/core/io/event_loop_tcp_client_socket_test.cc",
+    "src/quiche/quic/core/io/event_loop_connecting_client_socket_test.cc",
     "src/quiche/quic/core/io/quic_all_event_loops_test.cc",
     "src/quiche/quic/core/io/quic_poll_event_loop_test.cc",
     "src/quiche/quic/core/io/socket_test.cc",
diff --git a/build/source_list.json b/build/source_list.json
index 19cac11..df96337 100644
--- a/build/source_list.json
+++ b/build/source_list.json
@@ -948,8 +948,8 @@
     "quiche/quic/core/batch_writer/quic_batch_writer_test.h",
     "quiche/quic/core/batch_writer/quic_gso_batch_writer.h",
     "quiche/quic/core/batch_writer/quic_sendmmsg_batch_writer.h",
+    "quiche/quic/core/io/event_loop_connecting_client_socket.h",
     "quiche/quic/core/io/event_loop_socket_factory.h",
-    "quiche/quic/core/io/event_loop_tcp_client_socket.h",
     "quiche/quic/core/io/quic_default_event_loop.h",
     "quiche/quic/core/io/quic_event_loop.h",
     "quiche/quic/core/io/quic_poll_event_loop.h",
@@ -979,8 +979,8 @@
     "quiche/quic/core/batch_writer/quic_batch_writer_buffer.cc",
     "quiche/quic/core/batch_writer/quic_gso_batch_writer.cc",
     "quiche/quic/core/batch_writer/quic_sendmmsg_batch_writer.cc",
+    "quiche/quic/core/io/event_loop_connecting_client_socket.cc",
     "quiche/quic/core/io/event_loop_socket_factory.cc",
-    "quiche/quic/core/io/event_loop_tcp_client_socket.cc",
     "quiche/quic/core/io/quic_default_event_loop.cc",
     "quiche/quic/core/io/quic_poll_event_loop.cc",
     "quiche/quic/core/io/socket_posix.cc",
@@ -1291,7 +1291,7 @@
     "quiche/quic/core/http/quic_spdy_client_session_test.cc",
     "quiche/quic/core/http/quic_spdy_client_stream_test.cc",
     "quiche/quic/core/http/quic_spdy_server_stream_base_test.cc",
-    "quiche/quic/core/io/event_loop_tcp_client_socket_test.cc",
+    "quiche/quic/core/io/event_loop_connecting_client_socket_test.cc",
     "quiche/quic/core/io/quic_all_event_loops_test.cc",
     "quiche/quic/core/io/quic_poll_event_loop_test.cc",
     "quiche/quic/core/io/socket_test.cc",
diff --git a/quiche/quic/core/connecting_client_socket.h b/quiche/quic/core/connecting_client_socket.h
index 01c27dd..d5f8ee4 100644
--- a/quiche/quic/core/connecting_client_socket.h
+++ b/quiche/quic/core/connecting_client_socket.h
@@ -9,9 +9,10 @@
 
 #include "absl/status/status.h"
 #include "absl/status/statusor.h"
+#include "quiche/quic/core/quic_types.h"
+#include "quiche/quic/platform/api/quic_socket_address.h"
 #include "quiche/common/platform/api/quiche_export.h"
 #include "quiche/common/platform/api/quiche_mem_slice.h"
-#include "quiche/quic/core/quic_types.h"
 
 namespace quic {
 
@@ -73,6 +74,9 @@
   // platform behavior.
   virtual void Disconnect() = 0;
 
+  // Gets the address assigned to a connected socket.
+  virtual absl::StatusOr<QuicSocketAddress> GetLocalAddress() = 0;
+
   // Blocking read. Receives and returns a buffer of up to `max_size` bytes from
   // socket. Returns status on error.
   virtual absl::StatusOr<quiche::QuicheMemSlice> ReceiveBlocking(
diff --git a/quiche/quic/core/io/event_loop_tcp_client_socket.cc b/quiche/quic/core/io/event_loop_connecting_client_socket.cc
similarity index 89%
rename from quiche/quic/core/io/event_loop_tcp_client_socket.cc
rename to quiche/quic/core/io/event_loop_connecting_client_socket.cc
index 84263bc..aefa353 100644
--- a/quiche/quic/core/io/event_loop_tcp_client_socket.cc
+++ b/quiche/quic/core/io/event_loop_connecting_client_socket.cc
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "quiche/quic/core/io/event_loop_tcp_client_socket.h"
+#include "quiche/quic/core/io/event_loop_connecting_client_socket.h"
 
 #include <limits>
 #include <string>
@@ -21,12 +21,14 @@
 
 namespace quic {
 
-EventLoopTcpClientSocket::EventLoopTcpClientSocket(
+EventLoopConnectingClientSocket::EventLoopConnectingClientSocket(
+    socket_api::SocketProtocol protocol,
     const quic::QuicSocketAddress& peer_address,
     QuicByteCount receive_buffer_size, QuicByteCount send_buffer_size,
     QuicEventLoop* event_loop, quiche::QuicheBufferAllocator* buffer_allocator,
     AsyncVisitor* async_visitor)
-    : peer_address_(peer_address),
+    : protocol_(protocol),
+      peer_address_(peer_address),
       receive_buffer_size_(receive_buffer_size),
       send_buffer_size_(send_buffer_size),
       event_loop_(event_loop),
@@ -36,15 +38,15 @@
   QUICHE_DCHECK(buffer_allocator_);
 }
 
-EventLoopTcpClientSocket::~EventLoopTcpClientSocket() {
+EventLoopConnectingClientSocket::~EventLoopConnectingClientSocket() {
   // Connected socket must be closed via Disconnect() before destruction. Cannot
   // safely recover if state indicates caller may be expecting async callbacks.
   QUICHE_DCHECK(connect_status_ != ConnectStatus::kConnecting);
   QUICHE_DCHECK(!receive_max_size_.has_value());
   QUICHE_DCHECK(absl::holds_alternative<absl::monostate>(send_data_));
   if (descriptor_ != kInvalidSocketFd) {
-    QUICHE_BUG(quic_event_loop_tcp_socket_invalid_destruction)
-        << "Must call Disconnect() on connected TCP socket before destruction.";
+    QUICHE_BUG(quic_event_loop_connecting_socket_invalid_destruction)
+        << "Must call Disconnect() on connected socket before destruction.";
     Close();
   }
 
@@ -52,7 +54,7 @@
   QUICHE_DCHECK(send_remaining_.empty());
 }
 
-absl::Status EventLoopTcpClientSocket::ConnectBlocking() {
+absl::Status EventLoopConnectingClientSocket::ConnectBlocking() {
   QUICHE_DCHECK_EQ(descriptor_, kInvalidSocketFd);
   QUICHE_DCHECK(connect_status_ == ConnectStatus::kNotConnected);
   QUICHE_DCHECK(!receive_max_size_.has_value());
@@ -101,7 +103,7 @@
   return status;
 }
 
-void EventLoopTcpClientSocket::ConnectAsync() {
+void EventLoopConnectingClientSocket::ConnectAsync() {
   QUICHE_DCHECK(async_visitor_);
   QUICHE_DCHECK_EQ(descriptor_, kInvalidSocketFd);
   QUICHE_DCHECK(connect_status_ == ConnectStatus::kNotConnected);
@@ -117,7 +119,7 @@
   FinishOrRearmAsyncConnect(DoInitialConnect());
 }
 
-void EventLoopTcpClientSocket::Disconnect() {
+void EventLoopConnectingClientSocket::Disconnect() {
   QUICHE_DCHECK_NE(descriptor_, kInvalidSocketFd);
   QUICHE_DCHECK(connect_status_ != ConnectStatus::kNotConnected);
 
@@ -148,8 +150,16 @@
   }
 }
 
+absl::StatusOr<QuicSocketAddress>
+EventLoopConnectingClientSocket::GetLocalAddress() {
+  QUICHE_DCHECK_NE(descriptor_, kInvalidSocketFd);
+  QUICHE_DCHECK(connect_status_ == ConnectStatus::kConnected);
+
+  return socket_api::GetSocketAddress(descriptor_);
+}
+
 absl::StatusOr<quiche::QuicheMemSlice>
-EventLoopTcpClientSocket::ReceiveBlocking(QuicByteCount max_size) {
+EventLoopConnectingClientSocket::ReceiveBlocking(QuicByteCount max_size) {
   QUICHE_DCHECK_GT(max_size, 0u);
   QUICHE_DCHECK_NE(descriptor_, kInvalidSocketFd);
   QUICHE_DCHECK(connect_status_ == ConnectStatus::kConnected);
@@ -189,7 +199,7 @@
   return buffer;
 }
 
-void EventLoopTcpClientSocket::ReceiveAsync(QuicByteCount max_size) {
+void EventLoopConnectingClientSocket::ReceiveAsync(QuicByteCount max_size) {
   QUICHE_DCHECK(async_visitor_);
   QUICHE_DCHECK_GT(max_size, 0u);
   QUICHE_DCHECK_NE(descriptor_, kInvalidSocketFd);
@@ -201,7 +211,7 @@
   FinishOrRearmAsyncReceive(ReceiveInternal());
 }
 
-absl::Status EventLoopTcpClientSocket::SendBlocking(std::string data) {
+absl::Status EventLoopConnectingClientSocket::SendBlocking(std::string data) {
   QUICHE_DCHECK(!data.empty());
   QUICHE_DCHECK(absl::holds_alternative<absl::monostate>(send_data_));
 
@@ -209,7 +219,7 @@
   return SendBlockingInternal();
 }
 
-absl::Status EventLoopTcpClientSocket::SendBlocking(
+absl::Status EventLoopConnectingClientSocket::SendBlocking(
     quiche::QuicheMemSlice data) {
   QUICHE_DCHECK(!data.empty());
   QUICHE_DCHECK(absl::holds_alternative<absl::monostate>(send_data_));
@@ -218,7 +228,7 @@
   return SendBlockingInternal();
 }
 
-void EventLoopTcpClientSocket::SendAsync(std::string data) {
+void EventLoopConnectingClientSocket::SendAsync(std::string data) {
   QUICHE_DCHECK(!data.empty());
   QUICHE_DCHECK(absl::holds_alternative<absl::monostate>(send_data_));
 
@@ -228,7 +238,7 @@
   FinishOrRearmAsyncSend(SendInternal());
 }
 
-void EventLoopTcpClientSocket::SendAsync(quiche::QuicheMemSlice data) {
+void EventLoopConnectingClientSocket::SendAsync(quiche::QuicheMemSlice data) {
   QUICHE_DCHECK(!data.empty());
   QUICHE_DCHECK(absl::holds_alternative<absl::monostate>(send_data_));
 
@@ -239,9 +249,8 @@
   FinishOrRearmAsyncSend(SendInternal());
 }
 
-void EventLoopTcpClientSocket::OnSocketEvent(QuicEventLoop* event_loop,
-                                             SocketFd fd,
-                                             QuicSocketEventMask events) {
+void EventLoopConnectingClientSocket::OnSocketEvent(
+    QuicEventLoop* event_loop, SocketFd fd, QuicSocketEventMask events) {
   QUICHE_DCHECK_EQ(event_loop, event_loop_);
   QUICHE_DCHECK_EQ(fd, descriptor_);
 
@@ -261,16 +270,16 @@
   }
 }
 
-absl::Status EventLoopTcpClientSocket::Open() {
+absl::Status EventLoopConnectingClientSocket::Open() {
   QUICHE_DCHECK_EQ(descriptor_, kInvalidSocketFd);
   QUICHE_DCHECK(connect_status_ == ConnectStatus::kNotConnected);
   QUICHE_DCHECK(!receive_max_size_.has_value());
   QUICHE_DCHECK(absl::holds_alternative<absl::monostate>(send_data_));
   QUICHE_DCHECK(send_remaining_.empty());
 
-  absl::StatusOr<SocketFd> descriptor = socket_api::CreateSocket(
-      peer_address_.host().address_family(), socket_api::SocketProtocol::kTcp,
-      /*blocking=*/false);
+  absl::StatusOr<SocketFd> descriptor =
+      socket_api::CreateSocket(peer_address_.host().address_family(), protocol_,
+                               /*blocking=*/false);
   if (!descriptor.ok()) {
     QUICHE_DVLOG(1) << "Failed to open socket for connection to address: "
                     << peer_address_.ToString()
@@ -327,7 +336,7 @@
   return absl::OkStatus();
 }
 
-void EventLoopTcpClientSocket::Close() {
+void EventLoopConnectingClientSocket::Close() {
   QUICHE_DCHECK_NE(descriptor_, kInvalidSocketFd);
 
   bool unregistered = event_loop_->UnregisterSocket(descriptor_);
@@ -343,7 +352,7 @@
   descriptor_ = kInvalidSocketFd;
 }
 
-absl::Status EventLoopTcpClientSocket::DoInitialConnect() {
+absl::Status EventLoopConnectingClientSocket::DoInitialConnect() {
   QUICHE_DCHECK_NE(descriptor_, kInvalidSocketFd);
   QUICHE_DCHECK(connect_status_ == ConnectStatus::kNotConnected);
   QUICHE_DCHECK(!receive_max_size_.has_value());
@@ -366,7 +375,7 @@
   return connect_result;
 }
 
-absl::Status EventLoopTcpClientSocket::GetConnectResult() {
+absl::Status EventLoopConnectingClientSocket::GetConnectResult() {
   QUICHE_DCHECK_NE(descriptor_, kInvalidSocketFd);
   QUICHE_DCHECK(connect_status_ == ConnectStatus::kConnecting);
   QUICHE_DCHECK(!receive_max_size_.has_value());
@@ -414,7 +423,8 @@
   return error;
 }
 
-void EventLoopTcpClientSocket::FinishOrRearmAsyncConnect(absl::Status status) {
+void EventLoopConnectingClientSocket::FinishOrRearmAsyncConnect(
+    absl::Status status) {
   if (absl::IsUnavailable(status)) {
     if (!event_loop_->SupportsEdgeTriggered()) {
       bool result = event_loop_->RearmSocket(
@@ -429,7 +439,7 @@
 }
 
 absl::StatusOr<quiche::QuicheMemSlice>
-EventLoopTcpClientSocket::ReceiveInternal() {
+EventLoopConnectingClientSocket::ReceiveInternal() {
   QUICHE_DCHECK_NE(descriptor_, kInvalidSocketFd);
   QUICHE_DCHECK(connect_status_ == ConnectStatus::kConnected);
   QUICHE_CHECK(receive_max_size_.has_value());
@@ -473,7 +483,7 @@
   }
 }
 
-void EventLoopTcpClientSocket::FinishOrRearmAsyncReceive(
+void EventLoopConnectingClientSocket::FinishOrRearmAsyncReceive(
     absl::StatusOr<quiche::QuicheMemSlice> buffer) {
   QUICHE_DCHECK(async_visitor_);
   QUICHE_DCHECK(connect_status_ == ConnectStatus::kConnected);
@@ -491,7 +501,7 @@
   }
 }
 
-absl::StatusOr<bool> EventLoopTcpClientSocket::OneBytePeek() {
+absl::StatusOr<bool> EventLoopConnectingClientSocket::OneBytePeek() {
   QUICHE_DCHECK_NE(descriptor_, kInvalidSocketFd);
 
   char peek_buffer;
@@ -504,7 +514,7 @@
   }
 }
 
-absl::Status EventLoopTcpClientSocket::SendBlockingInternal() {
+absl::Status EventLoopConnectingClientSocket::SendBlockingInternal() {
   QUICHE_DCHECK_NE(descriptor_, kInvalidSocketFd);
   QUICHE_DCHECK(connect_status_ == ConnectStatus::kConnected);
   QUICHE_DCHECK(!absl::holds_alternative<absl::monostate>(send_data_));
@@ -552,7 +562,7 @@
   return status;
 }
 
-absl::Status EventLoopTcpClientSocket::SendInternal() {
+absl::Status EventLoopConnectingClientSocket::SendInternal() {
   QUICHE_DCHECK_NE(descriptor_, kInvalidSocketFd);
   QUICHE_DCHECK(connect_status_ == ConnectStatus::kConnected);
   QUICHE_DCHECK(!absl::holds_alternative<absl::monostate>(send_data_));
@@ -588,7 +598,8 @@
   return absl::OkStatus();
 }
 
-void EventLoopTcpClientSocket::FinishOrRearmAsyncSend(absl::Status status) {
+void EventLoopConnectingClientSocket::FinishOrRearmAsyncSend(
+    absl::Status status) {
   QUICHE_DCHECK(async_visitor_);
   QUICHE_DCHECK(connect_status_ == ConnectStatus::kConnected);
 
diff --git a/quiche/quic/core/io/event_loop_tcp_client_socket.h b/quiche/quic/core/io/event_loop_connecting_client_socket.h
similarity index 78%
rename from quiche/quic/core/io/event_loop_tcp_client_socket.h
rename to quiche/quic/core/io/event_loop_connecting_client_socket.h
index 213aac2..c85911c 100644
--- a/quiche/quic/core/io/event_loop_tcp_client_socket.h
+++ b/quiche/quic/core/io/event_loop_connecting_client_socket.h
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef QUICHE_QUIC_CORE_IO_EVENT_LOOP_TCP_CLIENT_SOCKET_H_
-#define QUICHE_QUIC_CORE_IO_EVENT_LOOP_TCP_CLIENT_SOCKET_H_
+#ifndef QUICHE_QUIC_CORE_IO_EVENT_LOOP_CONNECTING_CLIENT_SOCKET_H_
+#define QUICHE_QUIC_CORE_IO_EVENT_LOOP_CONNECTING_CLIENT_SOCKET_H_
 
 #include <string>
 
@@ -13,6 +13,7 @@
 #include "absl/types/variant.h"
 #include "quiche/quic/core/connecting_client_socket.h"
 #include "quiche/quic/core/io/quic_event_loop.h"
+#include "quiche/quic/core/io/socket.h"
 #include "quiche/quic/core/quic_types.h"
 #include "quiche/quic/platform/api/quic_socket_address.h"
 #include "quiche/common/platform/api/quiche_export.h"
@@ -20,8 +21,9 @@
 
 namespace quic {
 
-// A TCP client socket implemented using an underlying QuicEventLoop.
-class QUICHE_EXPORT_PRIVATE EventLoopTcpClientSocket
+// A connection-based client socket implemented using an underlying
+// QuicEventLoop.
+class QUICHE_EXPORT_PRIVATE EventLoopConnectingClientSocket
     : public ConnectingClientSocket,
       public QuicSocketEventListener {
  public:
@@ -29,19 +31,21 @@
   // `send_buffer_size` is zero. `async_visitor` may be null if no async
   // operations will be  requested. `event_loop`, `buffer_allocator`, and
   // `async_visitor` (if non-null) must outlive the created socket.
-  EventLoopTcpClientSocket(const quic::QuicSocketAddress& peer_address,
-                           QuicByteCount receive_buffer_size,
-                           QuicByteCount send_buffer_size,
-                           QuicEventLoop* event_loop,
-                           quiche::QuicheBufferAllocator* buffer_allocator,
-                           AsyncVisitor* async_visitor);
+  EventLoopConnectingClientSocket(
+      socket_api::SocketProtocol protocol,
+      const quic::QuicSocketAddress& peer_address,
+      QuicByteCount receive_buffer_size, QuicByteCount send_buffer_size,
+      QuicEventLoop* event_loop,
+      quiche::QuicheBufferAllocator* buffer_allocator,
+      AsyncVisitor* async_visitor);
 
-  ~EventLoopTcpClientSocket() override;
+  ~EventLoopConnectingClientSocket() override;
 
   // ConnectingClientSocket:
   absl::Status ConnectBlocking() override;
   void ConnectAsync() override;
   void Disconnect() override;
+  absl::StatusOr<QuicSocketAddress> GetLocalAddress() override;
   absl::StatusOr<quiche::QuicheMemSlice> ReceiveBlocking(
       QuicByteCount max_size) override;
   void ReceiveAsync(QuicByteCount max_size) override;
@@ -75,6 +79,7 @@
   absl::Status SendInternal();
   void FinishOrRearmAsyncSend(absl::Status status);
 
+  const socket_api::SocketProtocol protocol_;
   const QuicSocketAddress peer_address_;
   const QuicByteCount receive_buffer_size_;
   const QuicByteCount send_buffer_size_;
@@ -98,4 +103,4 @@
 
 }  // namespace quic
 
-#endif  // QUICHE_QUIC_CORE_IO_EVENT_LOOP_TCP_CLIENT_SOCKET_H_
+#endif  // QUICHE_QUIC_CORE_IO_EVENT_LOOP_CONNECTING_CLIENT_SOCKET_H_
diff --git a/quiche/quic/core/io/event_loop_connecting_client_socket_test.cc b/quiche/quic/core/io/event_loop_connecting_client_socket_test.cc
new file mode 100644
index 0000000..c99dc64
--- /dev/null
+++ b/quiche/quic/core/io/event_loop_connecting_client_socket_test.cc
@@ -0,0 +1,697 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "quiche/quic/core/io/event_loop_connecting_client_socket.h"
+
+#include <functional>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "absl/functional/bind_front.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "absl/types/span.h"
+#include "quiche/quic/core/connecting_client_socket.h"
+#include "quiche/quic/core/io/event_loop_socket_factory.h"
+#include "quiche/quic/core/io/quic_default_event_loop.h"
+#include "quiche/quic/core/io/quic_event_loop.h"
+#include "quiche/quic/core/io/socket.h"
+#include "quiche/quic/core/quic_time.h"
+#include "quiche/quic/platform/api/quic_ip_address_family.h"
+#include "quiche/quic/platform/api/quic_socket_address.h"
+#include "quiche/quic/test_tools/mock_clock.h"
+#include "quiche/quic/test_tools/quic_test_utils.h"
+#include "quiche/common/platform/api/quiche_logging.h"
+#include "quiche/common/platform/api/quiche_mem_slice.h"
+#include "quiche/common/platform/api/quiche_mutex.h"
+#include "quiche/common/platform/api/quiche_test.h"
+#include "quiche/common/platform/api/quiche_test_loopback.h"
+#include "quiche/common/platform/api/quiche_thread.h"
+#include "quiche/common/simple_buffer_allocator.h"
+
+namespace quic::test {
+namespace {
+
+using ::testing::Combine;
+using ::testing::Values;
+using ::testing::ValuesIn;
+
+class TestServerSocketRunner : public quiche::QuicheThread {
+ public:
+  using SocketBehavior = std::function<void(
+      SocketFd connected_socket, socket_api::SocketProtocol protocol)>;
+
+  TestServerSocketRunner(SocketFd server_socket_descriptor,
+                         SocketBehavior behavior)
+      : QuicheThread("TestServerSocketRunner"),
+        server_socket_descriptor_(server_socket_descriptor),
+        behavior_(std::move(behavior)) {
+    Start();
+  }
+
+  ~TestServerSocketRunner() override { WaitForCompletion(); }
+
+  void WaitForCompletion() { completion_notification_.WaitForNotification(); }
+
+ protected:
+  SocketFd server_socket_descriptor() const {
+    return server_socket_descriptor_;
+  }
+
+  const SocketBehavior& behavior() const { return behavior_; }
+
+  quiche::QuicheNotification& completion_notification() {
+    return completion_notification_;
+  }
+
+ private:
+  const SocketFd server_socket_descriptor_;
+  const SocketBehavior behavior_;
+
+  quiche::QuicheNotification completion_notification_;
+};
+
+class TestTcpServerSocketRunner : public TestServerSocketRunner {
+ public:
+  // On construction, spins a separate thread to accept a connection from
+  // `server_socket_descriptor`, runs `behavior` with that connection, and then
+  // closes the accepted connection socket.
+  TestTcpServerSocketRunner(SocketFd server_socket_descriptor,
+                            SocketBehavior behavior)
+      : TestServerSocketRunner(server_socket_descriptor, behavior) {}
+      ~TestTcpServerSocketRunner() override = default;
+
+ protected:
+  void Run() override {
+    AcceptSocket();
+    behavior()(connection_socket_descriptor_, socket_api::SocketProtocol::kTcp);
+    CloseSocket();
+
+    completion_notification().Notify();
+  }
+
+ private:
+  void AcceptSocket() {
+    absl::StatusOr<socket_api::AcceptResult> connection_socket =
+        socket_api::Accept(server_socket_descriptor(), /*blocking=*/true);
+    QUICHE_CHECK(connection_socket.ok());
+    connection_socket_descriptor_ = connection_socket.value().fd;
+  }
+
+  void CloseSocket() {
+    QUICHE_CHECK(socket_api::Close(connection_socket_descriptor_).ok());
+    QUICHE_CHECK(socket_api::Close(server_socket_descriptor()).ok());
+  }
+
+  SocketFd connection_socket_descriptor_ = kInvalidSocketFd;
+};
+
+class TestUdpServerSocketRunner : public TestServerSocketRunner {
+ public:
+  // On construction, spins a separate thread to connect
+  // `server_socket_descriptor` to `client_socket_address`, runs `behavior` with
+  // that connection, and then disconnects the socket.
+  TestUdpServerSocketRunner(SocketFd server_socket_descriptor,
+                            SocketBehavior behavior,
+                            QuicSocketAddress client_socket_address)
+      : TestServerSocketRunner(server_socket_descriptor, behavior),
+        client_socket_address_(std::move(client_socket_address)) {}
+  ~TestUdpServerSocketRunner() override = default;
+
+ protected:
+  void Run() override {
+    ConnectSocket();
+    behavior()(server_socket_descriptor(), socket_api::SocketProtocol::kUdp);
+    DisconnectSocket();
+
+    completion_notification().Notify();
+  }
+
+ private:
+  void ConnectSocket() {
+    QUICHE_CHECK(
+        socket_api::Connect(server_socket_descriptor(), client_socket_address_)
+            .ok());
+  }
+
+  void DisconnectSocket() {
+    QUICHE_CHECK(socket_api::Close(server_socket_descriptor()).ok());
+  }
+
+  QuicSocketAddress client_socket_address_;
+};
+
+class EventLoopConnectingClientSocketTest
+    : public quiche::test::QuicheTestWithParam<
+          std::tuple<socket_api::SocketProtocol, QuicEventLoopFactory*>>,
+      public ConnectingClientSocket::AsyncVisitor {
+ public:
+  void SetUp() override {
+    QuicEventLoopFactory* event_loop_factory;
+    std::tie(protocol_, event_loop_factory) = GetParam();
+
+    event_loop_ = event_loop_factory->Create(&clock_);
+    socket_factory_ = std::make_unique<EventLoopSocketFactory>(
+        event_loop_.get(), quiche::SimpleBufferAllocator::Get());
+
+    QUICHE_CHECK(CreateListeningServerSocket());
+  }
+
+  void TearDown() override {
+    if (server_socket_descriptor_ != kInvalidSocketFd) {
+      QUICHE_CHECK(socket_api::Close(server_socket_descriptor_).ok());
+    }
+  }
+
+  void ConnectComplete(absl::Status status) override {
+    QUICHE_CHECK(!connect_result_.has_value());
+    connect_result_ = std::move(status);
+  }
+
+  void ReceiveComplete(absl::StatusOr<quiche::QuicheMemSlice> data) override {
+    QUICHE_CHECK(!receive_result_.has_value());
+    receive_result_ = std::move(data);
+  }
+
+  void SendComplete(absl::Status status) override {
+    QUICHE_CHECK(!send_result_.has_value());
+    send_result_ = std::move(status);
+  }
+
+ protected:
+  std::unique_ptr<ConnectingClientSocket> CreateSocket(
+      const quic::QuicSocketAddress& peer_address,
+      ConnectingClientSocket::AsyncVisitor* async_visitor) {
+    switch (protocol_) {
+      case socket_api::SocketProtocol::kUdp:
+        return socket_factory_->CreateConnectingUdpClientSocket(
+            peer_address, /*receive_buffer_size=*/0, /*send_buffer_size=*/0,
+            async_visitor);
+      case socket_api::SocketProtocol::kTcp:
+        return socket_factory_->CreateTcpClientSocket(
+            peer_address, /*receive_buffer_size=*/0, /*send_buffer_size=*/0,
+            async_visitor);
+    }
+  }
+
+  std::unique_ptr<ConnectingClientSocket> CreateSocketToEncourageDelayedSend(
+      const quic::QuicSocketAddress& peer_address,
+      ConnectingClientSocket::AsyncVisitor* async_visitor) {
+    switch (protocol_) {
+      case socket_api::SocketProtocol::kUdp:
+        // Nothing special for UDP since UDP does not gaurantee packets will be
+        // sent once send buffers are full.
+        return socket_factory_->CreateConnectingUdpClientSocket(
+            peer_address, /*receive_buffer_size=*/0, /*send_buffer_size=*/0,
+            async_visitor);
+      case socket_api::SocketProtocol::kTcp:
+        // For TCP, set a very small send buffer to encourage sends to be
+        // delayed.
+        return socket_factory_->CreateTcpClientSocket(
+            peer_address, /*receive_buffer_size=*/0, /*send_buffer_size=*/4,
+            async_visitor);
+    }
+  }
+
+  bool CreateListeningServerSocket() {
+    absl::StatusOr<SocketFd> socket = socket_api::CreateSocket(
+        quiche::TestLoopback().address_family(), protocol_,
+        /*blocking=*/true);
+    QUICHE_CHECK(socket.ok());
+
+    // For TCP, set an extremely small receive buffer size to increase the odds
+    // of buffers filling up when testing asynchronous writes.
+    if (protocol_ == socket_api::SocketProtocol::kTcp) {
+      static const QuicByteCount kReceiveBufferSize = 2;
+      absl::Status result =
+          socket_api::SetReceiveBufferSize(socket.value(), kReceiveBufferSize);
+      QUICHE_CHECK(result.ok());
+    }
+
+    QuicSocketAddress bind_address(quiche::TestLoopback(), /*port=*/0);
+    absl::Status result = socket_api::Bind(socket.value(), bind_address);
+    QUICHE_CHECK(result.ok());
+
+    absl::StatusOr<QuicSocketAddress> socket_address =
+        socket_api::GetSocketAddress(socket.value());
+    QUICHE_CHECK(socket_address.ok());
+
+    // TCP sockets need to listen for connections. UDP sockets are ready to
+    // receive.
+    if (protocol_ == socket_api::SocketProtocol::kTcp) {
+      result = socket_api::Listen(socket.value(), /*backlog=*/1);
+      QUICHE_CHECK(result.ok());
+    }
+
+    server_socket_descriptor_ = socket.value();
+    server_socket_address_ = std::move(socket_address).value();
+    return true;
+  }
+
+  std::unique_ptr<TestServerSocketRunner> CreateServerSocketRunner(
+      TestServerSocketRunner::SocketBehavior behavior,
+      ConnectingClientSocket* client_socket) {
+    std::unique_ptr<TestServerSocketRunner> runner;
+    switch (protocol_) {
+      case socket_api::SocketProtocol::kUdp: {
+        absl::StatusOr<QuicSocketAddress> client_socket_address =
+            client_socket->GetLocalAddress();
+        QUICHE_CHECK(client_socket_address.ok());
+        runner = std::make_unique<TestUdpServerSocketRunner>(
+            server_socket_descriptor_, std::move(behavior),
+            std::move(client_socket_address).value());
+        break;
+      }
+      case socket_api::SocketProtocol::kTcp:
+        runner = std::make_unique<TestTcpServerSocketRunner>(
+            server_socket_descriptor_, std::move(behavior));
+        break;
+    }
+
+    // Runner takes responsibility for closing server socket.
+    server_socket_descriptor_ = kInvalidSocketFd;
+
+    return runner;
+  }
+
+  socket_api::SocketProtocol protocol_;
+
+  SocketFd server_socket_descriptor_ = kInvalidSocketFd;
+  QuicSocketAddress server_socket_address_;
+
+  MockClock clock_;
+  std::unique_ptr<QuicEventLoop> event_loop_;
+  std::unique_ptr<EventLoopSocketFactory> socket_factory_;
+
+  absl::optional<absl::Status> connect_result_;
+  absl::optional<absl::StatusOr<quiche::QuicheMemSlice>> receive_result_;
+  absl::optional<absl::Status> send_result_;
+};
+
+std::string GetTestParamName(
+    ::testing::TestParamInfo<
+        std::tuple<socket_api::SocketProtocol, QuicEventLoopFactory*>>
+        info) {
+  auto [protocol, event_loop_factory] = info.param;
+
+  return EscapeTestParamName(absl::StrCat(socket_api::GetProtocolName(protocol),
+                                          "_", event_loop_factory->GetName()));
+}
+
+INSTANTIATE_TEST_SUITE_P(EventLoopConnectingClientSocketTests,
+                         EventLoopConnectingClientSocketTest,
+                         Combine(Values(socket_api::SocketProtocol::kUdp,
+                                        socket_api::SocketProtocol::kTcp),
+                                 ValuesIn(GetAllSupportedEventLoops())),
+                         &GetTestParamName);
+
+TEST_P(EventLoopConnectingClientSocketTest, ConnectBlocking) {
+  std::unique_ptr<ConnectingClientSocket> socket =
+      CreateSocket(server_socket_address_,
+                   /*async_visitor=*/nullptr);
+
+  // No socket runner to accept the connection for the server, but that is not
+  // expected to be necessary for the connection to complete from the client for
+  // TCP or UDP.
+  EXPECT_TRUE(socket->ConnectBlocking().ok());
+
+  socket->Disconnect();
+}
+
+TEST_P(EventLoopConnectingClientSocketTest, ConnectAsync) {
+  std::unique_ptr<ConnectingClientSocket> socket =
+      CreateSocket(server_socket_address_,
+                   /*async_visitor=*/this);
+
+  socket->ConnectAsync();
+
+  // TCP connection typically completes asynchronously and UDP connection
+  // typically completes before ConnectAsync returns, but there is no simple way
+  // to ensure either behaves one way or the other. If connecting is
+  // asynchronous, expect completion once signalled by the event loop.
+  if (!connect_result_.has_value()) {
+    event_loop_->RunEventLoopOnce(QuicTime::Delta::FromSeconds(1));
+    ASSERT_TRUE(connect_result_.has_value());
+  }
+  EXPECT_TRUE(connect_result_.value().ok());
+
+  connect_result_.reset();
+  socket->Disconnect();
+  EXPECT_FALSE(connect_result_.has_value());
+}
+
+TEST_P(EventLoopConnectingClientSocketTest, ErrorBeforeConnectAsync) {
+  std::unique_ptr<ConnectingClientSocket> socket =
+      CreateSocket(server_socket_address_,
+                   /*async_visitor=*/this);
+
+  // Close the server socket.
+  EXPECT_TRUE(socket_api::Close(server_socket_descriptor_).ok());
+  server_socket_descriptor_ = kInvalidSocketFd;
+
+  socket->ConnectAsync();
+  if (!connect_result_.has_value()) {
+    event_loop_->RunEventLoopOnce(QuicTime::Delta::FromSeconds(1));
+    ASSERT_TRUE(connect_result_.has_value());
+  }
+
+  switch (protocol_) {
+    case socket_api::SocketProtocol::kTcp:
+      // Expect an error because server socket was closed before connection.
+      EXPECT_FALSE(connect_result_.value().ok());
+      break;
+    case socket_api::SocketProtocol::kUdp:
+      // No error for UDP because UDP connection success does not rely on the
+      // server.
+      EXPECT_TRUE(connect_result_.value().ok());
+      socket->Disconnect();
+      break;
+  }
+}
+
+TEST_P(EventLoopConnectingClientSocketTest, ErrorDuringConnectAsync) {
+  std::unique_ptr<ConnectingClientSocket> socket =
+      CreateSocket(server_socket_address_,
+                   /*async_visitor=*/this);
+
+  socket->ConnectAsync();
+
+  if (connect_result_.has_value()) {
+    // UDP typically completes connection immediately before this test has a
+    // chance to actually attempt the error. TCP typically completes
+    // asynchronously, but no simple way to ensure that always happens.
+    EXPECT_TRUE(connect_result_.value().ok());
+    socket->Disconnect();
+    return;
+  }
+
+  // Close the server socket.
+  EXPECT_TRUE(socket_api::Close(server_socket_descriptor_).ok());
+  server_socket_descriptor_ = kInvalidSocketFd;
+
+  EXPECT_FALSE(connect_result_.has_value());
+  event_loop_->RunEventLoopOnce(QuicTime::Delta::FromSeconds(1));
+  ASSERT_TRUE(connect_result_.has_value());
+
+  switch (protocol_) {
+    case socket_api::SocketProtocol::kTcp:
+      EXPECT_FALSE(connect_result_.value().ok());
+      break;
+    case socket_api::SocketProtocol::kUdp:
+      // No error for UDP because UDP connection success does not rely on the
+      // server.
+      EXPECT_TRUE(connect_result_.value().ok());
+      break;
+  }
+}
+
+TEST_P(EventLoopConnectingClientSocketTest, Disconnect) {
+  std::unique_ptr<ConnectingClientSocket> socket =
+      CreateSocket(server_socket_address_,
+                   /*async_visitor=*/nullptr);
+
+  ASSERT_TRUE(socket->ConnectBlocking().ok());
+  socket->Disconnect();
+}
+
+TEST_P(EventLoopConnectingClientSocketTest, DisconnectCancelsConnectAsync) {
+  std::unique_ptr<ConnectingClientSocket> socket =
+      CreateSocket(server_socket_address_,
+                   /*async_visitor=*/this);
+
+  socket->ConnectAsync();
+
+  bool expect_canceled = true;
+  if (connect_result_.has_value()) {
+    // UDP typically completes connection immediately before this test has a
+    // chance to actually attempt the disconnect. TCP typically completes
+    // asynchronously, but no simple way to ensure that always happens.
+    EXPECT_TRUE(connect_result_.value().ok());
+    expect_canceled = false;
+  }
+
+  socket->Disconnect();
+
+  if (expect_canceled) {
+    // Expect immediate cancelled error.
+    ASSERT_TRUE(connect_result_.has_value());
+    EXPECT_TRUE(absl::IsCancelled(connect_result_.value()));
+  }
+}
+
+TEST_P(EventLoopConnectingClientSocketTest, ConnectAndReconnect) {
+  std::unique_ptr<ConnectingClientSocket> socket =
+      CreateSocket(server_socket_address_,
+                   /*async_visitor=*/nullptr);
+
+  ASSERT_TRUE(socket->ConnectBlocking().ok());
+  socket->Disconnect();
+
+  // Expect `socket` can reconnect now that it has been disconnected.
+  EXPECT_TRUE(socket->ConnectBlocking().ok());
+  socket->Disconnect();
+}
+
+TEST_P(EventLoopConnectingClientSocketTest, GetLocalAddress) {
+  std::unique_ptr<ConnectingClientSocket> socket =
+      CreateSocket(server_socket_address_,
+                   /*async_visitor=*/nullptr);
+  ASSERT_TRUE(socket->ConnectBlocking().ok());
+
+  absl::StatusOr<QuicSocketAddress> address = socket->GetLocalAddress();
+  ASSERT_TRUE(address.ok());
+  EXPECT_TRUE(address.value().IsInitialized());
+
+  socket->Disconnect();
+}
+
+void SendDataOnSocket(absl::string_view data, SocketFd connected_socket,
+                      socket_api::SocketProtocol protocol) {
+  QUICHE_CHECK(!data.empty());
+
+  // May attempt to send in pieces for TCP. For UDP, expect failure if `data`
+  // cannot be sent in a single packet.
+  do {
+    absl::StatusOr<absl::string_view> remainder =
+        socket_api::Send(connected_socket, data);
+    if (!remainder.ok()) {
+      return;
+    }
+    data = remainder.value();
+  } while (protocol == socket_api::SocketProtocol::kTcp && !data.empty());
+
+  QUICHE_CHECK(data.empty());
+}
+
+TEST_P(EventLoopConnectingClientSocketTest, ReceiveBlocking) {
+  std::unique_ptr<ConnectingClientSocket> socket =
+      CreateSocket(server_socket_address_,
+                   /*async_visitor=*/nullptr);
+  ASSERT_TRUE(socket->ConnectBlocking().ok());
+
+  std::string expected = {1, 2, 3, 4, 5, 6, 7, 8};
+  std::unique_ptr<TestServerSocketRunner> runner = CreateServerSocketRunner(
+      absl::bind_front(&SendDataOnSocket, expected), socket.get());
+
+  std::string received;
+  absl::StatusOr<quiche::QuicheMemSlice> data;
+
+  // Expect exactly one packet for UDP, and at least two receives (data + FIN)
+  // for TCP.
+  do {
+    data = socket->ReceiveBlocking(100);
+    ASSERT_TRUE(data.ok());
+    received.append(data.value().data(), data.value().length());
+  } while (protocol_ == socket_api::SocketProtocol::kTcp &&
+           !data.value().empty());
+
+  EXPECT_EQ(received, expected);
+
+  socket->Disconnect();
+}
+
+TEST_P(EventLoopConnectingClientSocketTest, ReceiveAsync) {
+  std::unique_ptr<ConnectingClientSocket> socket =
+      CreateSocket(server_socket_address_,
+                   /*async_visitor=*/this);
+  ASSERT_TRUE(socket->ConnectBlocking().ok());
+
+  // Start an async receive.  Expect no immediate results because runner not
+  // yet setup to send.
+  socket->ReceiveAsync(100);
+  EXPECT_FALSE(receive_result_.has_value());
+
+  // Send data from server.
+  std::string expected = {1, 2, 3, 4, 5, 6, 7, 8};
+  std::unique_ptr<TestServerSocketRunner> runner = CreateServerSocketRunner(
+      absl::bind_front(&SendDataOnSocket, expected), socket.get());
+
+  EXPECT_FALSE(receive_result_.has_value());
+  for (int i = 0; i < 5 && !receive_result_.has_value(); ++i) {
+    event_loop_->RunEventLoopOnce(QuicTime::Delta::FromSeconds(1));
+  }
+
+  // Expect to receive at least some of the sent data.
+  ASSERT_TRUE(receive_result_.has_value());
+  ASSERT_TRUE(receive_result_.value().ok());
+  EXPECT_FALSE(receive_result_.value().value().empty());
+  std::string received(receive_result_.value().value().data(),
+                       receive_result_.value().value().length());
+
+  // For TCP, expect at least one more receive for the FIN.
+  if (protocol_ == socket_api::SocketProtocol::kTcp) {
+    absl::StatusOr<quiche::QuicheMemSlice> data;
+    do {
+      data = socket->ReceiveBlocking(100);
+      ASSERT_TRUE(data.ok());
+      received.append(data.value().data(), data.value().length());
+    } while (!data.value().empty());
+  }
+
+  EXPECT_EQ(received, expected);
+
+  receive_result_.reset();
+  socket->Disconnect();
+  EXPECT_FALSE(receive_result_.has_value());
+}
+
+TEST_P(EventLoopConnectingClientSocketTest, DisconnectCancelsReceiveAsync) {
+  std::unique_ptr<ConnectingClientSocket> socket =
+      CreateSocket(server_socket_address_,
+                   /*async_visitor=*/this);
+
+  ASSERT_TRUE(socket->ConnectBlocking().ok());
+
+  // Start an asynchronous read, expecting no completion because server never
+  // sends any data.
+  socket->ReceiveAsync(100);
+  EXPECT_FALSE(receive_result_.has_value());
+
+  // Disconnect and expect an immediate cancelled error.
+  socket->Disconnect();
+  ASSERT_TRUE(receive_result_.has_value());
+  ASSERT_FALSE(receive_result_.value().ok());
+  EXPECT_TRUE(absl::IsCancelled(receive_result_.value().status()));
+}
+
+// Receive from `connected_socket` until connection is closed, writing
+// received data to `out_received`.
+void ReceiveDataFromSocket(std::string* out_received, SocketFd connected_socket,
+                           socket_api::SocketProtocol protocol) {
+  out_received->clear();
+
+  std::string buffer(100, 0);
+  absl::StatusOr<absl::Span<char>> received;
+
+  // Expect exactly one packet for UDP, and at least two receives (data + FIN)
+  // for TCP.
+  do {
+    received = socket_api::Receive(connected_socket, absl::MakeSpan(buffer));
+    QUICHE_CHECK(received.ok());
+    out_received->insert(out_received->end(), received.value().begin(),
+                         received.value().end());
+  } while (protocol == socket_api::SocketProtocol::kTcp &&
+           !received.value().empty());
+  QUICHE_CHECK(!out_received->empty());
+}
+
+TEST_P(EventLoopConnectingClientSocketTest, SendBlocking) {
+  std::unique_ptr<ConnectingClientSocket> socket =
+      CreateSocket(server_socket_address_,
+                   /*async_visitor=*/nullptr);
+  ASSERT_TRUE(socket->ConnectBlocking().ok());
+
+  std::string sent;
+  std::unique_ptr<TestServerSocketRunner> runner = CreateServerSocketRunner(
+      absl::bind_front(&ReceiveDataFromSocket, &sent), socket.get());
+
+  std::string expected = {1, 2, 3, 4, 5, 6, 7, 8};
+  EXPECT_TRUE(socket->SendBlocking(expected).ok());
+  socket->Disconnect();
+
+  runner->WaitForCompletion();
+  EXPECT_EQ(sent, expected);
+}
+
+TEST_P(EventLoopConnectingClientSocketTest, SendAsync) {
+  std::unique_ptr<ConnectingClientSocket> socket =
+      CreateSocketToEncourageDelayedSend(server_socket_address_,
+                                         /*async_visitor=*/this);
+  ASSERT_TRUE(socket->ConnectBlocking().ok());
+
+  std::string data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+  std::string expected;
+
+  std::unique_ptr<TestServerSocketRunner> runner;
+  std::string sent;
+  switch (protocol_) {
+    case socket_api::SocketProtocol::kTcp:
+      // Repeatedly write to socket until it does not complete synchronously.
+      do {
+        expected.insert(expected.end(), data.begin(), data.end());
+        send_result_.reset();
+        socket->SendAsync(data);
+        ASSERT_TRUE(!send_result_.has_value() || send_result_.value().ok());
+      } while (send_result_.has_value());
+
+      // Begin receiving from server and expect more data to send.
+      runner = CreateServerSocketRunner(
+          absl::bind_front(&ReceiveDataFromSocket, &sent), socket.get());
+      EXPECT_FALSE(send_result_.has_value());
+      for (int i = 0; i < 5 && !send_result_.has_value(); ++i) {
+        event_loop_->RunEventLoopOnce(QuicTime::Delta::FromSeconds(1));
+      }
+      break;
+
+    case socket_api::SocketProtocol::kUdp:
+      // Expect UDP send to always send immediately.
+      runner = CreateServerSocketRunner(
+          absl::bind_front(&ReceiveDataFromSocket, &sent), socket.get());
+      socket->SendAsync(data);
+      expected = data;
+      break;
+  }
+  ASSERT_TRUE(send_result_.has_value());
+  EXPECT_TRUE(send_result_.value().ok());
+
+  send_result_.reset();
+  socket->Disconnect();
+  EXPECT_FALSE(send_result_.has_value());
+
+  runner->WaitForCompletion();
+  EXPECT_EQ(sent, expected);
+}
+
+TEST_P(EventLoopConnectingClientSocketTest, DisconnectCancelsSendAsync) {
+  if (protocol_ == socket_api::SocketProtocol::kUdp) {
+    // UDP sends are always immediate, so cannot disconect mid-send.
+    return;
+  }
+
+  std::unique_ptr<ConnectingClientSocket> socket =
+      CreateSocketToEncourageDelayedSend(server_socket_address_,
+                                         /*async_visitor=*/this);
+  ASSERT_TRUE(socket->ConnectBlocking().ok());
+
+  std::string data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+
+  // Repeatedly write to socket until it does not complete synchronously.
+  do {
+    send_result_.reset();
+    socket->SendAsync(data);
+    ASSERT_TRUE(!send_result_.has_value() || send_result_.value().ok());
+  } while (send_result_.has_value());
+
+  // Disconnect and expect immediate cancelled error.
+  socket->Disconnect();
+  ASSERT_TRUE(send_result_.has_value());
+  EXPECT_TRUE(absl::IsCancelled(send_result_.value()));
+}
+
+}  // namespace
+}  // namespace quic::test
diff --git a/quiche/quic/core/io/event_loop_socket_factory.cc b/quiche/quic/core/io/event_loop_socket_factory.cc
index 643efab..b1aaec7 100644
--- a/quiche/quic/core/io/event_loop_socket_factory.cc
+++ b/quiche/quic/core/io/event_loop_socket_factory.cc
@@ -7,8 +7,9 @@
 #include <memory>
 
 #include "quiche/quic/core/connecting_client_socket.h"
-#include "quiche/quic/core/io/event_loop_tcp_client_socket.h"
+#include "quiche/quic/core/io/event_loop_connecting_client_socket.h"
 #include "quiche/quic/core/io/quic_event_loop.h"
+#include "quiche/quic/core/io/socket.h"
 #include "quiche/quic/core/quic_types.h"
 #include "quiche/quic/platform/api/quic_socket_address.h"
 #include "quiche/common/platform/api/quiche_logging.h"
@@ -28,9 +29,19 @@
     const quic::QuicSocketAddress& peer_address,
     QuicByteCount receive_buffer_size, QuicByteCount send_buffer_size,
     ConnectingClientSocket::AsyncVisitor* async_visitor) {
-  return std::make_unique<EventLoopTcpClientSocket>(
-      peer_address, receive_buffer_size, send_buffer_size, event_loop_,
-      buffer_allocator_, async_visitor);
+  return std::make_unique<EventLoopConnectingClientSocket>(
+      socket_api::SocketProtocol::kTcp, peer_address, receive_buffer_size,
+      send_buffer_size, event_loop_, buffer_allocator_, async_visitor);
+}
+
+std::unique_ptr<ConnectingClientSocket>
+EventLoopSocketFactory::CreateConnectingUdpClientSocket(
+    const quic::QuicSocketAddress& peer_address,
+    QuicByteCount receive_buffer_size, QuicByteCount send_buffer_size,
+    ConnectingClientSocket::AsyncVisitor* async_visitor) {
+  return std::make_unique<EventLoopConnectingClientSocket>(
+      socket_api::SocketProtocol::kUdp, peer_address, receive_buffer_size,
+      send_buffer_size, event_loop_, buffer_allocator_, async_visitor);
 }
 
 }  // namespace quic
diff --git a/quiche/quic/core/io/event_loop_socket_factory.h b/quiche/quic/core/io/event_loop_socket_factory.h
index e3654f9..ee9a9f3 100644
--- a/quiche/quic/core/io/event_loop_socket_factory.h
+++ b/quiche/quic/core/io/event_loop_socket_factory.h
@@ -30,6 +30,10 @@
       const quic::QuicSocketAddress& peer_address,
       QuicByteCount receive_buffer_size, QuicByteCount send_buffer_size,
       ConnectingClientSocket::AsyncVisitor* async_visitor) override;
+  std::unique_ptr<ConnectingClientSocket> CreateConnectingUdpClientSocket(
+      const quic::QuicSocketAddress& peer_address,
+      QuicByteCount receive_buffer_size, QuicByteCount send_buffer_size,
+      ConnectingClientSocket::AsyncVisitor* async_visitor) override;
 
  private:
   QuicEventLoop* const event_loop_;                  // unowned
diff --git a/quiche/quic/core/io/event_loop_tcp_client_socket_test.cc b/quiche/quic/core/io/event_loop_tcp_client_socket_test.cc
deleted file mode 100644
index 68343a5..0000000
--- a/quiche/quic/core/io/event_loop_tcp_client_socket_test.cc
+++ /dev/null
@@ -1,523 +0,0 @@
-// Copyright 2022 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "quiche/quic/core/io/event_loop_tcp_client_socket.h"
-
-#include <functional>
-#include <memory>
-#include <utility>
-#include <vector>
-
-#include "absl/functional/bind_front.h"
-#include "absl/status/status.h"
-#include "absl/status/statusor.h"
-#include "absl/strings/string_view.h"
-#include "absl/types/optional.h"
-#include "absl/types/span.h"
-#include "quiche/quic/core/connecting_client_socket.h"
-#include "quiche/quic/core/io/event_loop_socket_factory.h"
-#include "quiche/quic/core/io/quic_default_event_loop.h"
-#include "quiche/quic/core/io/quic_event_loop.h"
-#include "quiche/quic/core/io/socket.h"
-#include "quiche/quic/core/quic_time.h"
-#include "quiche/quic/platform/api/quic_ip_address_family.h"
-#include "quiche/quic/platform/api/quic_socket_address.h"
-#include "quiche/quic/test_tools/mock_clock.h"
-#include "quiche/quic/test_tools/quic_test_utils.h"
-#include "quiche/common/platform/api/quiche_logging.h"
-#include "quiche/common/platform/api/quiche_mem_slice.h"
-#include "quiche/common/platform/api/quiche_mutex.h"
-#include "quiche/common/platform/api/quiche_test.h"
-#include "quiche/common/platform/api/quiche_test_loopback.h"
-#include "quiche/common/platform/api/quiche_thread.h"
-#include "quiche/common/simple_buffer_allocator.h"
-
-namespace quic::test {
-namespace {
-
-bool CreateListeningServerSocket(SocketFd* out_socket_descriptor,
-                                 QuicSocketAddress* out_socket_address) {
-  QUICHE_CHECK(out_socket_descriptor);
-  QUICHE_CHECK(out_socket_address);
-
-  absl::StatusOr<SocketFd> socket = socket_api::CreateSocket(
-      quiche::TestLoopback().address_family(), socket_api::SocketProtocol::kTcp,
-      /*blocking=*/true);
-  QUICHE_CHECK(socket.ok());
-
-  // Set an extremely small receive buffer size to increase the odds of buffers
-  // filling up when testing asynchronous writes.
-  static const QuicByteCount kReceiveBufferSize = 2;
-  absl::Status result =
-      socket_api::SetReceiveBufferSize(socket.value(), kReceiveBufferSize);
-  QUICHE_CHECK(result.ok());
-
-  QuicSocketAddress bind_address(quiche::TestLoopback(), /*port=*/0);
-  result = socket_api::Bind(socket.value(), bind_address);
-  QUICHE_CHECK(result.ok());
-
-  absl::StatusOr<QuicSocketAddress> socket_address =
-      socket_api::GetSocketAddress(socket.value());
-  QUICHE_CHECK(socket_address.ok());
-
-  result = socket_api::Listen(socket.value(), /*backlog=*/1);
-  QUICHE_CHECK(result.ok());
-
-  *out_socket_descriptor = socket.value();
-  *out_socket_address = std::move(socket_address).value();
-  return true;
-}
-
-class TestTcpServerSocketRunner : public quiche::QuicheThread {
- public:
-  using SocketBehavior = std::function<void(SocketFd connected_socket)>;
-
-  // On construction, spins a separate thread to accept a connection from
-  // `server_socket_descriptor`, runs `behavior` with that connection, and then
-  // closes the accepted connection socket. If `allow_accept_failure` is true,
-  // will silently stop if an error is encountered accepting the connection.
-  TestTcpServerSocketRunner(SocketFd server_socket_descriptor,
-                            SocketBehavior behavior,
-                            bool allow_accept_failure = false)
-      : QuicheThread("TestTcpServerSocketRunner"),
-        server_socket_descriptor_(server_socket_descriptor),
-        behavior_(std::move(behavior)),
-        allow_accept_failure_(allow_accept_failure) {
-    Start();
-  }
-
-  ~TestTcpServerSocketRunner() override { WaitForCompletion(); }
-
-  void WaitForCompletion() { completion_notification_.WaitForNotification(); }
-
- protected:
-  void Run() override {
-    if (AcceptSocket()) {
-      behavior_(connection_socket_descriptor_);
-      CloseSocket();
-    } else {
-      QUICHE_CHECK(allow_accept_failure_);
-    }
-
-    completion_notification_.Notify();
-  }
-
- private:
-  bool AcceptSocket() {
-    absl::StatusOr<socket_api::AcceptResult> connection_socket =
-        socket_api::Accept(server_socket_descriptor_, /*blocking=*/true);
-    if (connection_socket.ok()) {
-      connection_socket_descriptor_ = connection_socket.value().fd;
-    }
-    return connection_socket.ok();
-  }
-
-  void CloseSocket() {
-    QUICHE_CHECK(socket_api::Close(connection_socket_descriptor_).ok());
-  }
-
-  const SocketFd server_socket_descriptor_;
-  const SocketBehavior behavior_;
-  const bool allow_accept_failure_;
-
-  SocketFd connection_socket_descriptor_;
-
-  quiche::QuicheNotification completion_notification_;
-};
-
-class EventLoopTcpClientSocketTest
-    : public quiche::test::QuicheTestWithParam<QuicEventLoopFactory*>,
-      public ConnectingClientSocket::AsyncVisitor {
- public:
-  void SetUp() override {
-    QUICHE_CHECK(CreateListeningServerSocket(&server_socket_descriptor_,
-                                             &server_socket_address_));
-  }
-
-  void TearDown() override {
-    if (server_socket_descriptor_ != kInvalidSocketFd) {
-      QUICHE_CHECK(socket_api::Close(server_socket_descriptor_).ok());
-    }
-  }
-
-  void ConnectComplete(absl::Status status) override {
-    QUICHE_CHECK(!connect_result_.has_value());
-    connect_result_ = std::move(status);
-  }
-
-  void ReceiveComplete(absl::StatusOr<quiche::QuicheMemSlice> data) override {
-    QUICHE_CHECK(!receive_result_.has_value());
-    receive_result_ = std::move(data);
-  }
-
-  void SendComplete(absl::Status status) override {
-    QUICHE_CHECK(!send_result_.has_value());
-    send_result_ = std::move(status);
-  }
-
- protected:
-  SocketFd server_socket_descriptor_ = kInvalidSocketFd;
-  QuicSocketAddress server_socket_address_;
-
-  MockClock clock_;
-  std::unique_ptr<QuicEventLoop> event_loop_ = GetParam()->Create(&clock_);
-  EventLoopSocketFactory socket_factory_{event_loop_.get(),
-                                         quiche::SimpleBufferAllocator::Get()};
-
-  absl::optional<absl::Status> connect_result_;
-  absl::optional<absl::StatusOr<quiche::QuicheMemSlice>> receive_result_;
-  absl::optional<absl::Status> send_result_;
-};
-
-std::string GetTestParamName(
-    ::testing::TestParamInfo<QuicEventLoopFactory*> info) {
-  return EscapeTestParamName(info.param->GetName());
-}
-
-INSTANTIATE_TEST_SUITE_P(EventLoopTcpClientSocketTests,
-                         EventLoopTcpClientSocketTest,
-                         ::testing::ValuesIn(GetAllSupportedEventLoops()),
-                         &GetTestParamName);
-
-TEST_P(EventLoopTcpClientSocketTest, Connect) {
-  std::unique_ptr<ConnectingClientSocket> socket =
-      socket_factory_.CreateTcpClientSocket(server_socket_address_,
-                                            /*receive_buffer_size=*/0,
-                                            /*send_buffer_size=*/0,
-                                            /*async_visitor=*/nullptr);
-
-  // No socket runner to accept the connection for the server, but that is not
-  // expected to be necessary for the connection to complete from the client.
-  EXPECT_TRUE(socket->ConnectBlocking().ok());
-
-  socket->Disconnect();
-}
-
-TEST_P(EventLoopTcpClientSocketTest, ConnectAsync) {
-  std::unique_ptr<ConnectingClientSocket> socket =
-      socket_factory_.CreateTcpClientSocket(server_socket_address_,
-                                            /*receive_buffer_size=*/0,
-                                            /*send_buffer_size=*/0,
-                                            /*async_visitor=*/this);
-
-  socket->ConnectAsync();
-
-  // Synchronous completion not normally expected, but since there is no known
-  // way to delay the server side of the connection (the OS does not wait for
-  // an accept() call), cannot be gauranteed that the connection will always
-  // complete asynchronously. If connecting asynchronously (normal behavior),
-  // expect completion once signalled by the event loop.
-  if (!connect_result_.has_value()) {
-    event_loop_->RunEventLoopOnce(QuicTime::Delta::FromSeconds(1));
-    ASSERT_TRUE(connect_result_.has_value());
-  }
-  EXPECT_TRUE(connect_result_.value().ok());
-
-  connect_result_.reset();
-  socket->Disconnect();
-  EXPECT_FALSE(connect_result_.has_value());
-}
-
-TEST_P(EventLoopTcpClientSocketTest, ErrorBeforeConnectAsync) {
-  std::unique_ptr<ConnectingClientSocket> socket =
-      socket_factory_.CreateTcpClientSocket(server_socket_address_,
-                                            /*receive_buffer_size=*/0,
-                                            /*send_buffer_size=*/0,
-                                            /*async_visitor=*/this);
-
-  // Close the server socket.
-  EXPECT_TRUE(socket_api::Close(server_socket_descriptor_).ok());
-  server_socket_descriptor_ = kInvalidSocketFd;
-
-  socket->ConnectAsync();
-  if (!connect_result_.has_value()) {
-    event_loop_->RunEventLoopOnce(QuicTime::Delta::FromSeconds(1));
-    ASSERT_TRUE(connect_result_.has_value());
-  }
-
-  // Expect an error because server socket was closed before connection.
-  EXPECT_FALSE(connect_result_.value().ok());
-}
-
-TEST_P(EventLoopTcpClientSocketTest, ErrorDuringConnectAsync) {
-  std::unique_ptr<ConnectingClientSocket> socket =
-      socket_factory_.CreateTcpClientSocket(server_socket_address_,
-                                            /*receive_buffer_size=*/0,
-                                            /*send_buffer_size=*/0,
-                                            /*async_visitor=*/this);
-
-  socket->ConnectAsync();
-
-  if (connect_result_.has_value()) {
-    // Not typical, but theoretically nothing to stop the connection from
-    // completing before the server socket is closed to trigger the error.
-    EXPECT_TRUE(connect_result_.value().ok());
-    return;
-  }
-
-  // Close the server socket.
-  EXPECT_TRUE(socket_api::Close(server_socket_descriptor_).ok());
-  server_socket_descriptor_ = kInvalidSocketFd;
-
-  // Expect an error once signalled.
-  EXPECT_FALSE(connect_result_.has_value());
-  event_loop_->RunEventLoopOnce(QuicTime::Delta::FromSeconds(1));
-  ASSERT_TRUE(connect_result_.has_value());
-  EXPECT_FALSE(connect_result_.value().ok());
-}
-
-TEST_P(EventLoopTcpClientSocketTest, Disconnect) {
-  std::unique_ptr<ConnectingClientSocket> socket =
-      socket_factory_.CreateTcpClientSocket(server_socket_address_,
-                                            /*receive_buffer_size=*/0,
-                                            /*send_buffer_size=*/0,
-                                            /*async_visitor=*/nullptr);
-
-  ASSERT_TRUE(socket->ConnectBlocking().ok());
-  socket->Disconnect();
-}
-
-TEST_P(EventLoopTcpClientSocketTest, DisconnectCancelsConnectAsync) {
-  std::unique_ptr<ConnectingClientSocket> socket =
-      socket_factory_.CreateTcpClientSocket(server_socket_address_,
-                                            /*receive_buffer_size=*/0,
-                                            /*send_buffer_size=*/0,
-                                            /*async_visitor=*/this);
-
-  socket->ConnectAsync();
-
-  if (connect_result_.has_value()) {
-    // Not typical, but theoretically nothing to stop the connection from
-    // completing before the server socket is closed to trigger the error.
-    EXPECT_TRUE(connect_result_.value().ok());
-    return;
-  }
-
-  socket->Disconnect();
-
-  // Expect immediate cancelled error.
-  ASSERT_TRUE(connect_result_.has_value());
-  EXPECT_TRUE(absl::IsCancelled(connect_result_.value()));
-}
-
-TEST_P(EventLoopTcpClientSocketTest, ConnectAndReconnect) {
-  std::unique_ptr<ConnectingClientSocket> socket =
-      socket_factory_.CreateTcpClientSocket(server_socket_address_,
-                                            /*receive_buffer_size=*/0,
-                                            /*send_buffer_size=*/0,
-                                            /*async_visitor=*/nullptr);
-
-  ASSERT_TRUE(socket->ConnectBlocking().ok());
-  socket->Disconnect();
-
-  // Expect `socket` can reconnect now that it has been disconnected.
-  EXPECT_TRUE(socket->ConnectBlocking().ok());
-  socket->Disconnect();
-}
-
-void SendDataOnSocket(absl::string_view data, SocketFd connected_socket) {
-  while (!data.empty()) {
-    absl::StatusOr<absl::string_view> remainder =
-        socket_api::Send(connected_socket, data);
-    if (!remainder.ok()) {
-      return;
-    }
-    data = remainder.value();
-  }
-}
-
-TEST_P(EventLoopTcpClientSocketTest, Receive) {
-  std::string expected = {1, 2, 3, 4, 5, 6, 7, 8};
-  TestTcpServerSocketRunner runner(
-      server_socket_descriptor_, absl::bind_front(&SendDataOnSocket, expected));
-
-  std::unique_ptr<ConnectingClientSocket> socket =
-      socket_factory_.CreateTcpClientSocket(server_socket_address_,
-                                            /*receive_buffer_size=*/0,
-                                            /*send_buffer_size=*/0,
-                                            /*async_visitor=*/nullptr);
-  ASSERT_TRUE(socket->ConnectBlocking().ok());
-
-  std::string received;
-  absl::StatusOr<quiche::QuicheMemSlice> data;
-  do {
-    data = socket->ReceiveBlocking(100);
-    ASSERT_TRUE(data.ok());
-    received.append(data.value().data(), data.value().length());
-  } while (!data.value().empty());
-  EXPECT_EQ(received, expected);
-
-  socket->Disconnect();
-}
-
-TEST_P(EventLoopTcpClientSocketTest, ReceiveAsync) {
-  std::unique_ptr<ConnectingClientSocket> socket =
-      socket_factory_.CreateTcpClientSocket(server_socket_address_,
-                                            /*receive_buffer_size=*/0,
-                                            /*send_buffer_size=*/0,
-                                            /*async_visitor=*/this);
-  ASSERT_TRUE(socket->ConnectBlocking().ok());
-
-  // Start an async receive.  Expect no immediate results because runner not yet
-  // setup to accept and send.
-  socket->ReceiveAsync(100);
-  EXPECT_FALSE(receive_result_.has_value());
-
-  // Send data from server.
-  std::string expected = {1, 2, 3, 4, 5, 6, 7, 8};
-  TestTcpServerSocketRunner runner(
-      server_socket_descriptor_, absl::bind_front(&SendDataOnSocket, expected));
-  EXPECT_FALSE(receive_result_.has_value());
-  for (int i = 0; i < 5 && !receive_result_.has_value(); ++i) {
-    event_loop_->RunEventLoopOnce(QuicTime::Delta::FromSeconds(1));
-  }
-
-  // Expect to receive at least some of the sent data.
-  ASSERT_TRUE(receive_result_.has_value());
-  ASSERT_TRUE(receive_result_.value().ok());
-  EXPECT_FALSE(receive_result_.value().value().empty());
-  std::string received(receive_result_.value().value().data(),
-                       receive_result_.value().value().length());
-
-  // Get any remaining data via blocking calls.
-  absl::StatusOr<quiche::QuicheMemSlice> data;
-  do {
-    data = socket->ReceiveBlocking(100);
-    ASSERT_TRUE(data.ok());
-    received.append(data.value().data(), data.value().length());
-  } while (!data.value().empty());
-
-  EXPECT_EQ(received, expected);
-
-  receive_result_.reset();
-  socket->Disconnect();
-  EXPECT_FALSE(receive_result_.has_value());
-}
-
-TEST_P(EventLoopTcpClientSocketTest, DisconnectCancelsReceiveAsync) {
-  std::unique_ptr<ConnectingClientSocket> socket =
-      socket_factory_.CreateTcpClientSocket(server_socket_address_,
-                                            /*receive_buffer_size=*/0,
-                                            /*send_buffer_size=*/0,
-                                            /*async_visitor=*/this);
-
-  ASSERT_TRUE(socket->ConnectBlocking().ok());
-
-  // Start an asynchronous read, expecting no completion because server never
-  // sends any data.
-  socket->ReceiveAsync(100);
-  EXPECT_FALSE(receive_result_.has_value());
-
-  // Disconnect and expect an immediate cancelled error.
-  socket->Disconnect();
-  ASSERT_TRUE(receive_result_.has_value());
-  ASSERT_FALSE(receive_result_.value().ok());
-  EXPECT_TRUE(absl::IsCancelled(receive_result_.value().status()));
-}
-
-// Receive from `connected_socket` until connection is closed, writing received
-// data to `out_received`.
-void ReceiveDataFromSocket(std::string* out_received,
-                           SocketFd connected_socket) {
-  out_received->clear();
-
-  std::string buffer(100, 0);
-  absl::StatusOr<absl::Span<char>> received;
-  do {
-    received = socket_api::Receive(connected_socket, absl::MakeSpan(buffer));
-    QUICHE_CHECK(received.ok());
-    out_received->insert(out_received->end(), received.value().begin(),
-                         received.value().end());
-  } while (!received.value().empty());
-}
-
-TEST_P(EventLoopTcpClientSocketTest, Send) {
-  std::string sent;
-  TestTcpServerSocketRunner runner(
-      server_socket_descriptor_,
-      absl::bind_front(&ReceiveDataFromSocket, &sent));
-
-  std::unique_ptr<ConnectingClientSocket> socket =
-      socket_factory_.CreateTcpClientSocket(server_socket_address_,
-                                            /*receive_buffer_size=*/0,
-                                            /*send_buffer_size=*/0,
-                                            /*async_visitor=*/nullptr);
-  ASSERT_TRUE(socket->ConnectBlocking().ok());
-
-  std::string expected = {1, 2, 3, 4, 5, 6, 7, 8};
-  EXPECT_TRUE(socket->SendBlocking(expected).ok());
-  socket->Disconnect();
-
-  runner.WaitForCompletion();
-  EXPECT_EQ(sent, expected);
-}
-
-TEST_P(EventLoopTcpClientSocketTest, SendAsync) {
-  // Use a small send buffer to improve chances of a send needing to be
-  // asynchronous.
-  std::unique_ptr<ConnectingClientSocket> socket =
-      socket_factory_.CreateTcpClientSocket(server_socket_address_,
-                                            /*receive_buffer_size=*/0,
-                                            /*send_buffer_size=*/4,
-                                            /*async_visitor=*/this);
-  ASSERT_TRUE(socket->ConnectBlocking().ok());
-
-  std::string data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
-  std::string expected;
-
-  // Repeatedly write to socket until it does not complete synchronously.
-  do {
-    expected.insert(expected.end(), data.begin(), data.end());
-    send_result_.reset();
-    socket->SendAsync(data);
-    ASSERT_TRUE(!send_result_.has_value() || send_result_.value().ok());
-  } while (send_result_.has_value());
-
-  // Begin receiving from server and expect more data to send.
-  std::string sent;
-  TestTcpServerSocketRunner runner(
-      server_socket_descriptor_,
-      absl::bind_front(&ReceiveDataFromSocket, &sent));
-  EXPECT_FALSE(send_result_.has_value());
-  for (int i = 0; i < 5 && !send_result_.has_value(); ++i) {
-    event_loop_->RunEventLoopOnce(QuicTime::Delta::FromSeconds(1));
-  }
-  ASSERT_TRUE(send_result_.has_value());
-  EXPECT_TRUE(send_result_.value().ok());
-
-  send_result_.reset();
-  socket->Disconnect();
-  EXPECT_FALSE(send_result_.has_value());
-
-  runner.WaitForCompletion();
-  EXPECT_EQ(sent, expected);
-}
-
-TEST_P(EventLoopTcpClientSocketTest, DisconnectCancelsSendAsync) {
-  // Use a small send buffer to improve chances of a send needing to be
-  // asynchronous.
-  std::unique_ptr<ConnectingClientSocket> socket =
-      socket_factory_.CreateTcpClientSocket(server_socket_address_,
-                                            /*receive_buffer_size=*/0,
-                                            /*send_buffer_size=*/4,
-                                            /*async_visitor=*/this);
-  ASSERT_TRUE(socket->ConnectBlocking().ok());
-
-  std::string data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
-
-  // Repeatedly write to socket until it does not complete synchronously.
-  do {
-    send_result_.reset();
-    socket->SendAsync(data);
-    ASSERT_TRUE(!send_result_.has_value() || send_result_.value().ok());
-  } while (send_result_.has_value());
-
-  // Disconnect and expect immediate cancelled error.
-  socket->Disconnect();
-  ASSERT_TRUE(send_result_.has_value());
-  EXPECT_TRUE(absl::IsCancelled(send_result_.value()));
-}
-
-}  // namespace
-}  // namespace quic::test
diff --git a/quiche/quic/core/io/socket.h b/quiche/quic/core/io/socket.h
index a591723..8d32bc3 100644
--- a/quiche/quic/core/io/socket.h
+++ b/quiche/quic/core/io/socket.h
@@ -40,6 +40,17 @@
   kTcp,
 };
 
+inline absl::string_view GetProtocolName(SocketProtocol protocol) {
+  switch (protocol) {
+    case SocketProtocol::kUdp:
+      return "UDP";
+    case SocketProtocol::kTcp:
+      return "TCP";
+  }
+
+  return "unknown";
+}
+
 struct QUICHE_EXPORT_PRIVATE AcceptResult {
   // Socket for interacting with the accepted connection.
   SocketFd fd;
diff --git a/quiche/quic/core/socket_factory.h b/quiche/quic/core/socket_factory.h
index 4c473dc..d37499d 100644
--- a/quiche/quic/core/socket_factory.h
+++ b/quiche/quic/core/socket_factory.h
@@ -27,6 +27,19 @@
       const quic::QuicSocketAddress& peer_address,
       QuicByteCount receive_buffer_size, QuicByteCount send_buffer_size,
       ConnectingClientSocket::AsyncVisitor* async_visitor) = 0;
+
+  // Will use platform default buffer size if `receive_buffer_size` or
+  // `send_buffer_size` is zero. If `async_visitor` is null, async operations
+  // must not be called on the created socket. If `async_visitor` is non-null,
+  // it must outlive the created socket.
+  //
+  // TODO(ericorth): Consider creating a sub-interface for connecting UDP
+  // sockets with additional functionality, e.g. sendto, if needed.
+  virtual std::unique_ptr<ConnectingClientSocket>
+  CreateConnectingUdpClientSocket(
+      const quic::QuicSocketAddress& peer_address,
+      QuicByteCount receive_buffer_size, QuicByteCount send_buffer_size,
+      ConnectingClientSocket::AsyncVisitor* async_visitor) = 0;
 };
 
 }  // namespace quic
diff --git a/quiche/quic/tools/connect_tunnel_test.cc b/quiche/quic/tools/connect_tunnel_test.cc
index cb91e42..33753a1 100644
--- a/quiche/quic/tools/connect_tunnel_test.cc
+++ b/quiche/quic/tools/connect_tunnel_test.cc
@@ -69,6 +69,13 @@
                QuicByteCount send_buffer_size,
                ConnectingClientSocket::AsyncVisitor* async_visitor),
               (override));
+  MOCK_METHOD(std::unique_ptr<ConnectingClientSocket>,
+              CreateConnectingUdpClientSocket,
+              (const quic::QuicSocketAddress& peer_address,
+               QuicByteCount receive_buffer_size,
+               QuicByteCount send_buffer_size,
+               ConnectingClientSocket::AsyncVisitor* async_visitor),
+              (override));
 };
 
 class MockSocket : public ConnectingClientSocket {
@@ -76,6 +83,8 @@
   MOCK_METHOD(absl::Status, ConnectBlocking, (), (override));
   MOCK_METHOD(void, ConnectAsync, (), (override));
   MOCK_METHOD(void, Disconnect, (), (override));
+  MOCK_METHOD(absl::StatusOr<QuicSocketAddress>, GetLocalAddress, (),
+              (override));
   MOCK_METHOD(absl::StatusOr<quiche::QuicheMemSlice>, ReceiveBlocking,
               (QuicByteCount max_size), (override));
   MOCK_METHOD(void, ReceiveAsync, (QuicByteCount max_size), (override));