prototype implementation of connect-ethernet

https://www.ietf.org/id/draft-asedeno-masque-connect-ethernet-00.html

PiperOrigin-RevId: 550375485
diff --git a/quiche/quic/masque/masque_client_bin.cc b/quiche/quic/masque/masque_client_bin.cc
index a787afd..df74217 100644
--- a/quiche/quic/masque/masque_client_bin.cc
+++ b/quiche/quic/masque/masque_client_bin.cc
@@ -43,7 +43,7 @@
 DEFINE_QUICHE_COMMAND_LINE_FLAG(
     bool, bring_up_tun, false,
     "If set to true, no URLs need to be specified and instead a TUN device "
-    "is brought up with the assigned IP from the MASQUE CONNECT-IP server");
+    "is brought up with the assigned IP from the MASQUE CONNECT-IP server.");
 
 DEFINE_QUICHE_COMMAND_LINE_FLAG(
     bool, dns_on_client, false,
@@ -51,6 +51,11 @@
     "send the IP litteral in the CONNECT request. If set to false, "
     "masque_client send the hostname in the CONNECT request.");
 
+DEFINE_QUICHE_COMMAND_LINE_FLAG(
+    bool, bring_up_tap, false,
+    "If set to true, no URLs need to be specified and instead a TAP device "
+    "is brought up for a MASQUE CONNECT-ETHERNET session.");
+
 namespace quic {
 
 namespace {
@@ -117,7 +122,7 @@
       QUIC_DVLOG(1) << "Ignoring OnEvent fd " << fd << " event mask " << events;
       return;
     }
-    char datagram[1501];
+    char datagram[kMasqueIpPacketBufferSize];
     while (true) {
       ssize_t read_size = read(fd, datagram, sizeof(datagram));
       if (read_size < 0) {
@@ -142,6 +147,72 @@
   int fd_ = -1;
 };
 
+class MasqueTapSession
+    : public MasqueClientSession::EncapsulatedEthernetSession,
+      public QuicSocketEventListener {
+ public:
+  MasqueTapSession(QuicEventLoop* event_loop, MasqueClientSession* session)
+      : event_loop_(event_loop), session_(session) {}
+  ~MasqueTapSession() override = default;
+
+  void CreateInterface(void) {
+    QUIC_LOG(ERROR) << "Bringing up TAP";
+    fd_ = CreateTapInterface();
+    if (fd_ < 0) {
+      QUIC_LOG(FATAL) << "Failed to create TAP interface";
+    }
+    if (!event_loop_->RegisterSocket(fd_, kSocketEventReadable, this)) {
+      QUIC_LOG(FATAL) << "Failed to register TAP fd with the event loop";
+    }
+  }
+
+  // MasqueClientSession::EncapsulatedEthernetSession
+  void ProcessEthernetFrame(absl::string_view frame) override {
+    QUIC_LOG(INFO) << " Received Ethernet frame of length " << frame.length();
+    if (fd_ == -1) {
+      // TAP not open, early return
+      return;
+    }
+    if (write(fd_, frame.data(), frame.size()) == -1) {
+      QUIC_LOG(FATAL) << "Failed to write";
+    }
+  }
+  void CloseEthernetSession(const std::string& details) override {
+    QUIC_LOG(ERROR) << "Was asked to close Ethernet session: " << details;
+  }
+
+  // QuicSocketEventListener
+  void OnSocketEvent(QuicEventLoop* /*event_loop*/, QuicUdpSocketFd fd,
+                     QuicSocketEventMask events) override {
+    if ((events & kSocketEventReadable) == 0) {
+      QUIC_DVLOG(1) << "Ignoring OnEvent fd " << fd << " event mask " << events;
+      return;
+    }
+    char datagram[kMasqueEthernetFrameBufferSize];
+    while (true) {
+      ssize_t read_size = read(fd, datagram, sizeof(datagram));
+      if (read_size < 0) {
+        break;
+      }
+      // Frame received from the TAP. Write it to the MASQUE CONNECT-ETHERNET
+      // session.
+      session_->SendEthernetFrame(absl::string_view(datagram, read_size), this);
+    }
+    if (!event_loop_->SupportsEdgeTriggered()) {
+      if (!event_loop_->RearmSocket(fd, kSocketEventReadable)) {
+        QUIC_BUG(MasqueServerSession_ConnectIp_OnSocketEvent_Rearm)
+            << "Failed to re-arm socket " << fd << " for reading";
+      }
+    }
+  }
+
+ private:
+  QuicEventLoop* event_loop_;
+  MasqueClientSession* session_;
+  std::string local_mac_address_;  // string, uint8_t[6], or new wrapper type?
+  int fd_ = -1;
+};
+
 int RunMasqueClient(int argc, char* argv[]) {
   const char* usage = "Usage: masque_client [options] <url>";
 
@@ -153,7 +224,12 @@
   std::vector<std::string> urls =
       quiche::QuicheParseCommandLineFlags(usage, argc, argv);
   bool bring_up_tun = quiche::GetQuicheCommandLineFlag(FLAGS_bring_up_tun);
-  if (urls.empty() && !bring_up_tun) {
+  bool bring_up_tap = quiche::GetQuicheCommandLineFlag(FLAGS_bring_up_tap);
+  if (urls.empty() && !bring_up_tun && !bring_up_tap) {
+    quiche::QuichePrintCommandLineFlagHelp(usage);
+    return 1;
+  }
+  if (bring_up_tun && bring_up_tap) {
     quiche::QuichePrintCommandLineFlagHelp(usage);
     return 1;
   }
@@ -197,6 +273,9 @@
       masque_mode = MasqueMode::kOpen;
     } else if (mode_string == "connectip" || mode_string == "connect-ip") {
       masque_mode = MasqueMode::kConnectIp;
+    } else if (mode_string == "connectethernet" ||
+               mode_string == "connect-ethernet") {
+      masque_mode = MasqueMode::kConnectEthernet;
     } else {
       QUIC_LOG(ERROR) << "Invalid masque_mode \"" << mode_string << "\"";
       return 1;
@@ -235,6 +314,15 @@
     }
     QUICHE_NOTREACHED();
   }
+  if (bring_up_tap) {
+    MasqueTapSession tap_session(event_loop.get(),
+                                 masque_client->masque_client_session());
+    tap_session.CreateInterface();
+    while (true) {
+      event_loop->RunEventLoopOnce(QuicTime::Delta::FromMilliseconds(50));
+    }
+    QUICHE_NOTREACHED();
+  }
 
   const bool dns_on_client =
       quiche::GetQuicheCommandLineFlag(FLAGS_dns_on_client);
diff --git a/quiche/quic/masque/masque_client_session.cc b/quiche/quic/masque/masque_client_session.cc
index 83f1648..d023741 100644
--- a/quiche/quic/masque/masque_client_session.cc
+++ b/quiche/quic/masque/masque_client_session.cc
@@ -32,6 +32,7 @@
 using ::quiche::RouteAdvertisementCapsule;
 
 constexpr uint64_t kConnectIpPayloadContextId = 0;
+constexpr uint64_t kConnectEthernetPayloadContextId = 0;
 }  // namespace
 
 MasqueClientSession::MasqueClientSession(
@@ -204,6 +205,54 @@
   return &connect_ip_client_states_.back();
 }
 
+const MasqueClientSession::ConnectEthernetClientState*
+MasqueClientSession::GetOrCreateConnectEthernetClientState(
+    MasqueClientSession::EncapsulatedEthernetSession*
+        encapsulated_ethernet_session) {
+  for (const ConnectEthernetClientState& client_state :
+       connect_ethernet_client_states_) {
+    if (client_state.encapsulated_ethernet_session() ==
+        encapsulated_ethernet_session) {
+      // Found existing CONNECT-ETHERNET request.
+      return &client_state;
+    }
+  }
+  // No CONNECT-ETHERNET request found, create a new one.
+  QuicSpdyClientStream* stream = CreateOutgoingBidirectionalStream();
+  if (stream == nullptr) {
+    // Stream flow control limits prevented us from opening a new stream.
+    QUIC_DLOG(ERROR) << "Failed to open CONNECT-ETHERNET stream";
+    return nullptr;
+  }
+
+  QuicUrl url(uri_template_);
+  std::string scheme = url.scheme();
+  std::string authority = url.HostPort();
+  std::string path = "/.well-known/masque/ethernet/";
+
+  QUIC_DLOG(INFO) << "Sending CONNECT-ETHERNET request on stream "
+                  << stream->id() << " scheme=\"" << scheme << "\" authority=\""
+                  << authority << "\" path=\"" << path << "\"";
+
+  // Send the request.
+  spdy::Http2HeaderBlock headers;
+  headers[":method"] = "CONNECT";
+  headers[":protocol"] = "connect-ethernet";
+  headers[":scheme"] = scheme;
+  headers[":authority"] = authority;
+  headers[":path"] = path;
+  size_t bytes_sent =
+      stream->SendRequest(std::move(headers), /*body=*/"", /*fin=*/false);
+  if (bytes_sent == 0) {
+    QUIC_DLOG(ERROR) << "Failed to send CONNECT-ETHERNET request";
+    return nullptr;
+  }
+
+  connect_ethernet_client_states_.push_back(
+      ConnectEthernetClientState(stream, encapsulated_ethernet_session, this));
+  return &connect_ethernet_client_states_.back();
+}
+
 void MasqueClientSession::SendIpPacket(
     absl::string_view packet,
     MasqueClientSession::EncapsulatedIpSession* encapsulated_ip_session) {
@@ -236,6 +285,39 @@
                 << MessageStatusToString(message_status);
 }
 
+void MasqueClientSession::SendEthernetFrame(
+    absl::string_view frame, MasqueClientSession::EncapsulatedEthernetSession*
+                                 encapsulated_ethernet_session) {
+  const ConnectEthernetClientState* connect_ethernet =
+      GetOrCreateConnectEthernetClientState(encapsulated_ethernet_session);
+  if (connect_ethernet == nullptr) {
+    QUIC_DLOG(ERROR) << "Failed to create CONNECT-ETHERNET request";
+    return;
+  }
+
+  std::string http_payload;
+  http_payload.resize(
+      QuicDataWriter::GetVarInt62Len(kConnectEthernetPayloadContextId) +
+      frame.size());
+  QuicDataWriter writer(http_payload.size(), http_payload.data());
+  if (!writer.WriteVarInt62(kConnectEthernetPayloadContextId)) {
+    QUIC_BUG(IP context write fail)
+        << "Failed to write CONNECT-ETHERNET context ID";
+    return;
+  }
+  if (!writer.WriteStringPiece(frame)) {
+    QUIC_BUG(IP packet write fail) << "Failed to write CONNECT-ETHERNET frame";
+    return;
+  }
+  MessageStatus message_status =
+      SendHttp3Datagram(connect_ethernet->stream()->id(), http_payload);
+
+  QUIC_DVLOG(1) << "Sent encapsulated Ethernet frame of length " << frame.size()
+                << " with stream ID " << connect_ethernet->stream()->id()
+                << " and got message status "
+                << MessageStatusToString(message_status);
+}
+
 void MasqueClientSession::SendPacket(
     absl::string_view packet, const QuicSocketAddress& target_server_address,
     EncapsulatedClientSession* encapsulated_client_session) {
@@ -295,6 +377,24 @@
   }
 }
 
+void MasqueClientSession::CloseConnectEthernetStream(
+    EncapsulatedEthernetSession* encapsulated_ethernet_session) {
+  for (auto it = connect_ethernet_client_states_.begin();
+       it != connect_ethernet_client_states_.end();) {
+    if (it->encapsulated_ethernet_session() == encapsulated_ethernet_session) {
+      QUIC_DLOG(INFO) << "Removing CONNECT-ETHERNET state for stream ID "
+                      << it->stream()->id();
+      auto* stream = it->stream();
+      it = connect_ethernet_client_states_.erase(it);
+      if (!stream->write_side_closed()) {
+        stream->Reset(QUIC_STREAM_CANCELLED);
+      }
+    } else {
+      ++it;
+    }
+  }
+}
+
 void MasqueClientSession::OnConnectionClosed(
     const QuicConnectionCloseFrame& frame, ConnectionCloseSource source) {
   QuicSpdyClientSession::OnConnectionClosed(frame, source);
@@ -502,6 +602,65 @@
 
 void MasqueClientSession::ConnectIpClientState::OnHeadersWritten() {}
 
+// ConnectEthernetClientState
+
+MasqueClientSession::ConnectEthernetClientState::ConnectEthernetClientState(
+    QuicSpdyClientStream* stream,
+    EncapsulatedEthernetSession* encapsulated_ethernet_session,
+    MasqueClientSession* masque_session)
+    : stream_(stream),
+      encapsulated_ethernet_session_(encapsulated_ethernet_session),
+      masque_session_(masque_session) {
+  QUICHE_DCHECK_NE(masque_session_, nullptr);
+  this->stream()->RegisterHttp3DatagramVisitor(this);
+}
+
+MasqueClientSession::ConnectEthernetClientState::~ConnectEthernetClientState() {
+  if (stream() != nullptr) {
+    stream()->UnregisterHttp3DatagramVisitor();
+  }
+}
+
+MasqueClientSession::ConnectEthernetClientState::ConnectEthernetClientState(
+    MasqueClientSession::ConnectEthernetClientState&& other) {
+  *this = std::move(other);
+}
+
+MasqueClientSession::ConnectEthernetClientState&
+MasqueClientSession::ConnectEthernetClientState::operator=(
+    MasqueClientSession::ConnectEthernetClientState&& other) {
+  stream_ = other.stream_;
+  encapsulated_ethernet_session_ = other.encapsulated_ethernet_session_;
+  masque_session_ = other.masque_session_;
+  other.stream_ = nullptr;
+  if (stream() != nullptr) {
+    stream()->ReplaceHttp3DatagramVisitor(this);
+  }
+  return *this;
+}
+
+void MasqueClientSession::ConnectEthernetClientState::OnHttp3Datagram(
+    QuicStreamId stream_id, absl::string_view payload) {
+  QUICHE_DCHECK_EQ(stream_id, stream()->id());
+  QuicDataReader reader(payload);
+  uint64_t context_id;
+  if (!reader.ReadVarInt62(&context_id)) {
+    QUIC_DLOG(ERROR) << "Failed to read context ID";
+    return;
+  }
+  if (context_id != kConnectEthernetPayloadContextId) {
+    QUIC_DLOG(ERROR) << "Ignoring HTTP Datagram with unexpected context ID "
+                     << context_id;
+    return;
+  }
+  absl::string_view http_payload = reader.ReadRemainingPayload();
+  encapsulated_ethernet_session_->ProcessEthernetFrame(http_payload);
+  QUIC_DVLOG(1) << "Sent " << http_payload.size()
+                << " ETHERNET bytes to connection for stream ID " << stream_id;
+}
+
+// End ConnectEthernetClientState
+
 quiche::QuicheIpAddress MasqueClientSession::GetFakeAddress(
     absl::string_view hostname) {
   quiche::QuicheIpAddress address;
diff --git a/quiche/quic/masque/masque_client_session.h b/quiche/quic/masque/masque_client_session.h
index 4e317eb..742ae41 100644
--- a/quiche/quic/masque/masque_client_session.h
+++ b/quiche/quic/masque/masque_client_session.h
@@ -73,6 +73,19 @@
         const quiche::RouteAdvertisementCapsule& capsule) = 0;
   };
 
+  // CONNECT-ETHERNET.
+  class QUIC_NO_EXPORT EncapsulatedEthernetSession {
+   public:
+    virtual ~EncapsulatedEthernetSession() {}
+
+    // Process packet that was just decapsulated. |frame| contains the
+    // Ethernet header and payload.
+    virtual void ProcessEthernetFrame(absl::string_view frame) = 0;
+
+    // Close the encapsulated connection.
+    virtual void CloseEthernetSession(const std::string& details) = 0;
+  };
+
   // Takes ownership of |connection|, but not of |crypto_config| or
   // |push_promise_index| or |owner|. All pointers must be non-null. Caller
   // must ensure that |push_promise_index| and |owner| stay valid for the
@@ -109,6 +122,12 @@
   void SendIpPacket(absl::string_view packet,
                     EncapsulatedIpSession* encapsulated_ip_session);
 
+  // Send encapsulated Ethernet frame. |frame| contains the Ethernet
+  // header and payload.
+  void SendEthernetFrame(
+      absl::string_view frame,
+      EncapsulatedEthernetSession* encapsulated_ethernet_session);
+
   // Close CONNECT-UDP stream tied to this encapsulated client session.
   void CloseConnectUdpStream(
       EncapsulatedClientSession* encapsulated_client_session);
@@ -116,6 +135,10 @@
   // Close CONNECT-IP stream tied to this encapsulated client session.
   void CloseConnectIpStream(EncapsulatedIpSession* encapsulated_ip_session);
 
+  // Close CONNECT-ETHERNET stream tied to this encapsulated client session.
+  void CloseConnectEthernetStream(
+      EncapsulatedEthernetSession* encapsulated_ethernet_session);
+
   // Generate a random Unique Local Address and register a mapping from
   // that address to the corresponding hostname. The returned address should be
   // removed by calling RemoveFakeAddress() once it is no longer needed.
@@ -212,6 +235,43 @@
     MasqueClientSession* masque_session_;             // Unowned.
   };
 
+  // State that the MasqueClientSession keeps for each CONNECT-ETHERNET request.
+  class QUIC_NO_EXPORT ConnectEthernetClientState
+      : public QuicSpdyStream::Http3DatagramVisitor {
+   public:
+    // |stream| and |encapsulated_client_session| must be valid for the lifetime
+    // of the ConnectUdpClientState.
+    explicit ConnectEthernetClientState(
+        QuicSpdyClientStream* stream,
+        EncapsulatedEthernetSession* encapsulated_ethernet_session,
+        MasqueClientSession* masque_session);
+
+    ~ConnectEthernetClientState();
+
+    // Disallow copy but allow move.
+    ConnectEthernetClientState(const ConnectEthernetClientState&) = delete;
+    ConnectEthernetClientState(ConnectEthernetClientState&&);
+    ConnectEthernetClientState& operator=(const ConnectEthernetClientState&) =
+        delete;
+    ConnectEthernetClientState& operator=(ConnectEthernetClientState&&);
+
+    QuicSpdyClientStream* stream() const { return stream_; }
+    EncapsulatedEthernetSession* encapsulated_ethernet_session() const {
+      return encapsulated_ethernet_session_;
+    }
+
+    // From QuicSpdyStream::Http3DatagramVisitor.
+    void OnHttp3Datagram(QuicStreamId stream_id,
+                         absl::string_view payload) override;
+    void OnUnknownCapsule(QuicStreamId /*stream_id*/,
+                          const quiche::UnknownCapsule& /*capsule*/) override {}
+
+   private:
+    QuicSpdyClientStream* stream_;                                // Unowned.
+    EncapsulatedEthernetSession* encapsulated_ethernet_session_;  // Unowned.
+    MasqueClientSession* masque_session_;                         // Unowned.
+  };
+
   HttpDatagramSupport LocalHttpDatagramSupport() override {
     return HttpDatagramSupport::kRfc;
   }
@@ -223,10 +283,14 @@
   const ConnectIpClientState* GetOrCreateConnectIpClientState(
       EncapsulatedIpSession* encapsulated_ip_session);
 
+  const ConnectEthernetClientState* GetOrCreateConnectEthernetClientState(
+      EncapsulatedEthernetSession* encapsulated_ethernet_session);
+
   MasqueMode masque_mode_;
   std::string uri_template_;
   std::list<ConnectUdpClientState> connect_udp_client_states_;
   std::list<ConnectIpClientState> connect_ip_client_states_;
+  std::list<ConnectEthernetClientState> connect_ethernet_client_states_;
   // Maps fake addresses generated by GetFakeAddress() to their corresponding
   // hostnames.
   absl::flat_hash_map<std::string, std::string> fake_addresses_;
diff --git a/quiche/quic/masque/masque_server_backend.cc b/quiche/quic/masque/masque_server_backend.cc
index d7ccb9a..a40ecc0 100644
--- a/quiche/quic/masque/masque_server_backend.cc
+++ b/quiche/quic/masque/masque_server_backend.cc
@@ -37,7 +37,8 @@
   auto protocol_pair = request_headers.find(":protocol");
   if (method != "CONNECT" || protocol_pair == request_headers.end() ||
       (protocol_pair->second != "connect-udp" &&
-       protocol_pair->second != "connect-ip")) {
+       protocol_pair->second != "connect-ip" &&
+       protocol_pair->second != "connect-ethernet")) {
     // This is not a MASQUE request.
     return false;
   }
diff --git a/quiche/quic/masque/masque_server_session.cc b/quiche/quic/masque/masque_server_session.cc
index c3d7b06..b2cb5f2 100644
--- a/quiche/quic/masque/masque_server_session.cc
+++ b/quiche/quic/masque/masque_server_session.cc
@@ -145,6 +145,10 @@
       [stream_id](const ConnectIpServerState& connect_ip) {
         return connect_ip.stream()->id() == stream_id;
       });
+  connect_ethernet_server_states_.remove_if(
+      [stream_id](const ConnectEthernetServerState& connect_ethernet) {
+        return connect_ethernet.stream()->id() == stream_id;
+      });
 
   QuicSimpleServerSession::OnStreamClosed(stream_id);
 }
@@ -193,7 +197,8 @@
     QUIC_DLOG(ERROR) << "MASQUE request with bad method \"" << method << "\"";
     return CreateBackendErrorResponse("400", "Bad method");
   }
-  if (protocol != "connect-udp" && protocol != "connect-ip") {
+  if (protocol != "connect-udp" && protocol != "connect-ip" &&
+      protocol != "connect-ethernet") {
     QUIC_DLOG(ERROR) << "MASQUE request with bad protocol \"" << protocol
                      << "\"";
     return CreateBackendErrorResponse("400", "Bad protocol");
@@ -235,6 +240,39 @@
 
     return response;
   }
+  if (protocol == "connect-ethernet") {
+    QuicSpdyStream* stream = static_cast<QuicSpdyStream*>(
+        GetActiveStream(request_handler->stream_id()));
+    if (stream == nullptr) {
+      QUIC_BUG(bad masque server stream type)
+          << "Unexpected stream type for stream ID "
+          << request_handler->stream_id();
+      return CreateBackendErrorResponse("500", "Bad stream type");
+    }
+    int fd = CreateTapInterface();
+    if (fd < 0) {
+      QUIC_LOG(ERROR) << "Failed to create TAP interface for stream ID "
+                      << request_handler->stream_id();
+      return CreateBackendErrorResponse("500",
+                                        "Failed to create TAP interface");
+    }
+    if (!event_loop_->RegisterSocket(fd, kSocketEventReadable, this)) {
+      QUIC_DLOG(ERROR) << "Failed to register TAP fd with the event loop";
+      close(fd);
+      return CreateBackendErrorResponse("500", "Registering TAP socket failed");
+    }
+    connect_ethernet_server_states_.push_back(
+        ConnectEthernetServerState(stream, fd, this));
+
+    spdy::Http2HeaderBlock response_headers;
+    response_headers[":status"] = "200";
+    auto response = std::make_unique<QuicBackendResponse>();
+    response->set_response_type(QuicBackendResponse::INCOMPLETE_RESPONSE);
+    response->set_headers(std::move(response_headers));
+    response->set_body("");
+
+    return response;
+  }
   // Extract target host and port from path using default template.
   std::vector<absl::string_view> path_split = absl::StrSplit(path, '/');
   if (path_split.size() != 7 || !path_split[0].empty() ||
@@ -335,7 +373,8 @@
   });
 
   if (!(HandleConnectUdpSocketEvent(fd, events) ||
-        HandleConnectIpSocketEvent(fd, events))) {
+        HandleConnectIpSocketEvent(fd, events) ||
+        HandleConnectEthernetSocketEvent(fd, events))) {
     QUIC_BUG(MasqueServerSession_OnSocketEvent_UnhandledEvent)
         << "Got unexpected event mask " << events << " on unknown fd " << fd;
     std::move(rearm).Cancel();
@@ -391,7 +430,7 @@
           << "Unexpected incoming UDP packet on fd " << fd << " from "
           << expected_target_server_address
           << " because MASQUE connection is closed";
-      break;
+      return true;
     }
     // The packet is valid, send it to the client in a DATAGRAM frame.
     MessageStatus message_status =
@@ -417,7 +456,7 @@
   }
   QUIC_DVLOG(1) << "Received readable event on fd " << fd << " (mask " << events
                 << ") stream ID " << it->stream()->id();
-  char datagram[1501];
+  char datagram[kMasqueIpPacketBufferSize];
   datagram[0] = 0;  // Context ID.
   while (true) {
     ssize_t read_size = read(fd, datagram + 1, sizeof(datagram) - 1);
@@ -434,6 +473,35 @@
   return true;
 }
 
+bool MasqueServerSession::HandleConnectEthernetSocketEvent(
+    QuicUdpSocketFd fd, QuicSocketEventMask events) {
+  auto it =
+      absl::c_find_if(connect_ethernet_server_states_,
+                      [fd](const ConnectEthernetServerState& connect_ethernet) {
+                        return connect_ethernet.fd() == fd;
+                      });
+  if (it == connect_ethernet_server_states_.end()) {
+    return false;
+  }
+  QUIC_DVLOG(1) << "Received readable event on fd " << fd << " (mask " << events
+                << ") stream ID " << it->stream()->id();
+  char datagram[kMasqueEthernetFrameBufferSize];
+  datagram[0] = 0;  // Context ID.
+  while (true) {
+    ssize_t read_size = read(fd, datagram + 1, sizeof(datagram) - 1);
+    if (read_size < 0) {
+      break;
+    }
+    MessageStatus message_status = it->stream()->SendHttp3Datagram(
+        absl::string_view(datagram, 1 + read_size));
+    QUIC_DVLOG(1) << "Encapsulated Ethernet frame of length " << read_size
+                  << " with stream ID " << it->stream()->id()
+                  << " and got message status "
+                  << MessageStatusToString(message_status);
+  }
+  return true;
+}
+
 bool MasqueServerSession::OnSettingsFrame(const SettingsFrame& frame) {
   QUIC_DLOG(INFO) << "Received SETTINGS: " << frame;
   if (!QuicSimpleServerSession::OnSettingsFrame(frame)) {
@@ -649,4 +717,82 @@
   stream()->WriteCapsule(route_advertisement);
 }
 
+// Connect Ethernet
+MasqueServerSession::ConnectEthernetServerState::ConnectEthernetServerState(
+    QuicSpdyStream* stream, QuicUdpSocketFd fd,
+    MasqueServerSession* masque_session)
+    : stream_(stream), fd_(fd), masque_session_(masque_session) {
+  QUICHE_DCHECK_NE(fd_, kQuicInvalidSocketFd);
+  QUICHE_DCHECK_NE(masque_session_, nullptr);
+  this->stream()->RegisterHttp3DatagramVisitor(this);
+}
+
+MasqueServerSession::ConnectEthernetServerState::~ConnectEthernetServerState() {
+  if (stream() != nullptr) {
+    stream()->UnregisterHttp3DatagramVisitor();
+  }
+  if (fd_ == kQuicInvalidSocketFd) {
+    return;
+  }
+  QuicUdpSocketApi socket_api;
+  QUIC_DLOG(INFO) << "Closing fd " << fd_;
+  if (!masque_session_->event_loop()->UnregisterSocket(fd_)) {
+    QUIC_DLOG(ERROR) << "Failed to unregister FD " << fd_;
+  }
+  socket_api.Destroy(fd_);
+}
+
+MasqueServerSession::ConnectEthernetServerState::ConnectEthernetServerState(
+    MasqueServerSession::ConnectEthernetServerState&& other) {
+  fd_ = kQuicInvalidSocketFd;
+  *this = std::move(other);
+}
+
+MasqueServerSession::ConnectEthernetServerState&
+MasqueServerSession::ConnectEthernetServerState::operator=(
+    MasqueServerSession::ConnectEthernetServerState&& other) {
+  if (fd_ != kQuicInvalidSocketFd) {
+    QuicUdpSocketApi socket_api;
+    QUIC_DLOG(INFO) << "Closing fd " << fd_;
+    if (!masque_session_->event_loop()->UnregisterSocket(fd_)) {
+      QUIC_DLOG(ERROR) << "Failed to unregister FD " << fd_;
+    }
+    socket_api.Destroy(fd_);
+  }
+  stream_ = other.stream_;
+  other.stream_ = nullptr;
+  fd_ = other.fd_;
+  masque_session_ = other.masque_session_;
+  other.fd_ = kQuicInvalidSocketFd;
+  if (stream() != nullptr) {
+    stream()->ReplaceHttp3DatagramVisitor(this);
+  }
+  return *this;
+}
+
+void MasqueServerSession::ConnectEthernetServerState::OnHttp3Datagram(
+    QuicStreamId stream_id, absl::string_view payload) {
+  QUICHE_DCHECK_EQ(stream_id, stream()->id());
+  QuicDataReader reader(payload);
+  uint64_t context_id;
+  if (!reader.ReadVarInt62(&context_id)) {
+    QUIC_DLOG(ERROR) << "Failed to read context ID";
+    return;
+  }
+  if (context_id != 0) {
+    QUIC_DLOG(ERROR) << "Ignoring HTTP Datagram with unexpected context ID "
+                     << context_id;
+    return;
+  }
+  absl::string_view ethernet_frame = reader.ReadRemainingPayload();
+  ssize_t written = write(fd(), ethernet_frame.data(), ethernet_frame.size());
+  if (written != static_cast<ssize_t>(ethernet_frame.size())) {
+    QUIC_DLOG(ERROR) << "Failed to write CONNECT-ETHERNET packet of length "
+                     << ethernet_frame.size();
+  } else {
+    QUIC_DLOG(INFO) << "Decapsulated CONNECT-ETHERNET packet of length "
+                    << ethernet_frame.size();
+  }
+}
+
 }  // namespace quic
diff --git a/quiche/quic/masque/masque_server_session.h b/quiche/quic/masque/masque_server_session.h
index 4b5ad94..0226f9a 100644
--- a/quiche/quic/masque/masque_server_session.h
+++ b/quiche/quic/masque/masque_server_session.h
@@ -59,6 +59,8 @@
                                    QuicSocketEventMask events);
   bool HandleConnectIpSocketEvent(QuicUdpSocketFd fd,
                                   QuicSocketEventMask events);
+  bool HandleConnectEthernetSocketEvent(QuicUdpSocketFd fd,
+                                        QuicSocketEventMask events);
 
   // State that the MasqueServerSession keeps for each CONNECT-UDP request.
   class QUIC_NO_EXPORT ConnectUdpServerState
@@ -141,6 +143,40 @@
     MasqueServerSession* masque_session_;  // Unowned.
   };
 
+  // State that the MasqueServerSession keeps for each CONNECT-ETHERNET request.
+  class QUIC_NO_EXPORT ConnectEthernetServerState
+      : public QuicSpdyStream::Http3DatagramVisitor {
+   public:
+    // ConnectEthernetServerState takes ownership of |fd|. It will unregister it
+    // from |event_loop| and close the file descriptor when destructed.
+    explicit ConnectEthernetServerState(QuicSpdyStream* stream,
+                                        QuicUdpSocketFd fd,
+                                        MasqueServerSession* masque_session);
+
+    ~ConnectEthernetServerState();
+
+    // Disallow copy but allow move.
+    ConnectEthernetServerState(const ConnectEthernetServerState&) = delete;
+    ConnectEthernetServerState(ConnectEthernetServerState&&);
+    ConnectEthernetServerState& operator=(const ConnectEthernetServerState&) =
+        delete;
+    ConnectEthernetServerState& operator=(ConnectEthernetServerState&&);
+
+    QuicSpdyStream* stream() const { return stream_; }
+    QuicUdpSocketFd fd() const { return fd_; }
+
+    // From QuicSpdyStream::Http3DatagramVisitor.
+    void OnHttp3Datagram(QuicStreamId stream_id,
+                         absl::string_view payload) override;
+    void OnUnknownCapsule(QuicStreamId /*stream_id*/,
+                          const quiche::UnknownCapsule& /*capsule*/) override {}
+
+   private:
+    QuicSpdyStream* stream_;
+    QuicUdpSocketFd fd_;                   // Owned.
+    MasqueServerSession* masque_session_;  // Unowned.
+  };
+
   // From QuicSpdySession.
   bool OnSettingsFrame(const SettingsFrame& frame) override;
   HttpDatagramSupport LocalHttpDatagramSupport() override {
@@ -152,6 +188,7 @@
   MasqueMode masque_mode_;
   std::list<ConnectUdpServerState> connect_udp_server_states_;
   std::list<ConnectIpServerState> connect_ip_server_states_;
+  std::list<ConnectEthernetServerState> connect_ethernet_server_states_;
   bool masque_initialized_ = false;
 };
 
diff --git a/quiche/quic/masque/masque_utils.cc b/quiche/quic/masque/masque_utils.cc
index 9811a2d..03d60e0 100644
--- a/quiche/quic/masque/masque_utils.cc
+++ b/quiche/quic/masque/masque_utils.cc
@@ -11,6 +11,8 @@
 #include <sys/ioctl.h>
 #endif  // defined(__linux__)
 
+#include "absl/cleanup/cleanup.h"
+
 namespace quic {
 
 ParsedQuicVersionVector MasqueSupportedVersions() {
@@ -41,6 +43,8 @@
       return "Open";
     case MasqueMode::kConnectIp:
       return "CONNECT-IP";
+    case MasqueMode::kConnectEthernet:
+      return "CONNECT-ETHERNET";
   }
   return absl::StrCat("Unknown(", static_cast<int>(masque_mode), ")");
 }
@@ -58,87 +62,82 @@
   }
   // TODO(b/281517862): add test to validate O_NONBLOCK
   int tun_fd = open("/dev/net/tun", O_RDWR | O_NONBLOCK);
-  int ip_fd = -1;
-  do {
-    if (tun_fd < 0) {
-      QUIC_PLOG(ERROR) << "Failed to open clone device";
-      break;
-    }
-    struct ifreq ifr = {};
-    ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
-    // If we want to pick a specific device name, we can set it via
-    // ifr.ifr_name. Otherwise, the kernel will pick the next available tunX
-    // name.
-    int err = ioctl(tun_fd, TUNSETIFF, &ifr);
-    if (err < 0) {
-      QUIC_PLOG(ERROR) << "TUNSETIFF failed";
-      break;
-    }
-    ip_fd = socket(AF_INET, SOCK_DGRAM, 0);
-    if (ip_fd < 0) {
-      QUIC_PLOG(ERROR) << "Failed to open IP configuration socket";
-      break;
-    }
-    struct sockaddr_in addr = {};
-    addr.sin_family = AF_INET;
-    // Local address, unused but needs to be set. We use the same address as the
-    // client address, but with last byte set to 1.
-    addr.sin_addr = client_address.GetIPv4();
-    if (server) {
-      addr.sin_addr.s_addr &= htonl(0xffffff00);
-      addr.sin_addr.s_addr |= htonl(0x00000001);
-    }
-    memcpy(&ifr.ifr_addr, &addr, sizeof(addr));
-    err = ioctl(ip_fd, SIOCSIFADDR, &ifr);
-    if (err < 0) {
-      QUIC_PLOG(ERROR) << "SIOCSIFADDR failed";
-      break;
-    }
-    // Peer address, needs to match source IP address of sent packets.
-    addr.sin_addr = client_address.GetIPv4();
-    if (!server) {
-      addr.sin_addr.s_addr &= htonl(0xffffff00);
-      addr.sin_addr.s_addr |= htonl(0x00000001);
-    }
-    memcpy(&ifr.ifr_addr, &addr, sizeof(addr));
-    err = ioctl(ip_fd, SIOCSIFDSTADDR, &ifr);
-    if (err < 0) {
-      QUIC_PLOG(ERROR) << "SIOCSIFDSTADDR failed";
-      break;
-    }
-    if (!server) {
-      // Set MTU, to 1280 for now which should always fit (fingers crossed)
-      ifr.ifr_mtu = 1280;
-      err = ioctl(ip_fd, SIOCSIFMTU, &ifr);
-      if (err < 0) {
-        QUIC_PLOG(ERROR) << "SIOCSIFMTU failed";
-        break;
-      }
-    }
+  if (tun_fd < 0) {
+    QUIC_PLOG(ERROR) << "Failed to open clone device";
+    return -1;
+  }
+  absl::Cleanup tun_fd_closer = [tun_fd] { close(tun_fd); };
 
-    err = ioctl(ip_fd, SIOCGIFFLAGS, &ifr);
-    if (err < 0) {
-      QUIC_PLOG(ERROR) << "SIOCGIFFLAGS failed";
-      break;
-    }
-    ifr.ifr_flags |= (IFF_UP | IFF_RUNNING);
-    err = ioctl(ip_fd, SIOCSIFFLAGS, &ifr);
-    if (err < 0) {
-      QUIC_PLOG(ERROR) << "SIOCSIFFLAGS failed";
-      break;
-    }
-    close(ip_fd);
-    QUIC_DLOG(INFO) << "Successfully created TUN interface " << ifr.ifr_name
-                    << " with fd " << tun_fd;
-    return tun_fd;
-  } while (false);
-  if (tun_fd >= 0) {
-    close(tun_fd);
+  struct ifreq ifr = {};
+  ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
+  // If we want to pick a specific device name, we can set it via
+  // ifr.ifr_name. Otherwise, the kernel will pick the next available tunX
+  // name.
+  int err = ioctl(tun_fd, TUNSETIFF, &ifr);
+  if (err < 0) {
+    QUIC_PLOG(ERROR) << "TUNSETIFF failed";
+    return -1;
   }
-  if (ip_fd >= 0) {
-    close(ip_fd);
+  int ip_fd = socket(AF_INET, SOCK_DGRAM, 0);
+  if (ip_fd < 0) {
+    QUIC_PLOG(ERROR) << "Failed to open IP configuration socket";
+    return -1;
   }
-  return -1;
+  absl::Cleanup ip_fd_closer = [ip_fd] { close(ip_fd); };
+
+  struct sockaddr_in addr = {};
+  addr.sin_family = AF_INET;
+  // Local address, unused but needs to be set. We use the same address as the
+  // client address, but with last byte set to 1.
+  addr.sin_addr = client_address.GetIPv4();
+  if (server) {
+    addr.sin_addr.s_addr &= htonl(0xffffff00);
+    addr.sin_addr.s_addr |= htonl(0x00000001);
+  }
+  memcpy(&ifr.ifr_addr, &addr, sizeof(addr));
+  err = ioctl(ip_fd, SIOCSIFADDR, &ifr);
+  if (err < 0) {
+    QUIC_PLOG(ERROR) << "SIOCSIFADDR failed";
+    return -1;
+  }
+  // Peer address, needs to match source IP address of sent packets.
+  addr.sin_addr = client_address.GetIPv4();
+  if (!server) {
+    addr.sin_addr.s_addr &= htonl(0xffffff00);
+    addr.sin_addr.s_addr |= htonl(0x00000001);
+  }
+  memcpy(&ifr.ifr_addr, &addr, sizeof(addr));
+  err = ioctl(ip_fd, SIOCSIFDSTADDR, &ifr);
+  if (err < 0) {
+    QUIC_PLOG(ERROR) << "SIOCSIFDSTADDR failed";
+    return -1;
+  }
+  if (!server) {
+    // Set MTU, to 1280 for now which should always fit (fingers crossed)
+    ifr.ifr_mtu = 1280;
+    err = ioctl(ip_fd, SIOCSIFMTU, &ifr);
+    if (err < 0) {
+      QUIC_PLOG(ERROR) << "SIOCSIFMTU failed";
+      return -1;
+    }
+  }
+
+  err = ioctl(ip_fd, SIOCGIFFLAGS, &ifr);
+  if (err < 0) {
+    QUIC_PLOG(ERROR) << "SIOCGIFFLAGS failed";
+    return -1;
+  }
+  ifr.ifr_flags |= (IFF_UP | IFF_RUNNING);
+  err = ioctl(ip_fd, SIOCSIFFLAGS, &ifr);
+  if (err < 0) {
+    QUIC_PLOG(ERROR) << "SIOCSIFFLAGS failed";
+    return -1;
+  }
+  close(ip_fd);
+  QUIC_DLOG(INFO) << "Successfully created TUN interface " << ifr.ifr_name
+                  << " with fd " << tun_fd;
+  std::move(tun_fd_closer).Cancel();
+  return tun_fd;
 }
 #else
 int CreateTunInterface(const QuicIpAddress& /*client_address*/,
@@ -148,4 +147,62 @@
 }
 #endif  // defined(__linux__)
 
+#if defined(__linux__)
+int CreateTapInterface() {
+  int tap_fd = open("/dev/net/tun", O_RDWR | O_NONBLOCK);
+  if (tap_fd < 0) {
+    QUIC_PLOG(ERROR) << "Failed to open clone device";
+    return -1;
+  }
+  absl::Cleanup tap_fd_closer = [tap_fd] { close(tap_fd); };
+
+  struct ifreq ifr = {};
+  ifr.ifr_flags = IFF_TAP | IFF_NO_PI;
+  // If we want to pick a specific device name, we can set it via
+  // ifr.ifr_name. Otherwise, the kernel will pick the next available tapX
+  // name.
+  int err = ioctl(tap_fd, TUNSETIFF, &ifr);
+  if (err < 0) {
+    QUIC_PLOG(ERROR) << "TUNSETIFF failed";
+    return -1;
+  }
+
+  QUIC_DLOG(INFO) << "Successfully created TAP interface " << ifr.ifr_name
+                  << " with fd " << tap_fd;
+
+  int sock_fd = socket(AF_UNIX, SOCK_DGRAM, 0);
+  if (sock_fd < 0) {
+    QUIC_PLOG(ERROR) << "Error opening configuration socket";
+    return -1;
+  }
+  absl::Cleanup sock_fd_closer = [sock_fd] { close(sock_fd); };
+
+  ifr.ifr_mtu = 1280;
+  err = ioctl(sock_fd, SIOCSIFMTU, &ifr);
+  if (err < 0) {
+    QUIC_PLOG(ERROR) << "SIOCSIFMTU failed";
+    return -1;
+  }
+
+  err = ioctl(sock_fd, SIOCGIFFLAGS, &ifr);
+  if (err < 0) {
+    QUIC_PLOG(ERROR) << "SIOCGIFFLAGS failed";
+    return -1;
+  }
+  ifr.ifr_flags |= (IFF_UP | IFF_RUNNING);
+  err = ioctl(sock_fd, SIOCSIFFLAGS, &ifr);
+  if (err < 0) {
+    QUIC_PLOG(ERROR) << "SIOCSIFFLAGS failed";
+    return -1;
+  }
+  std::move(tap_fd_closer).Cancel();
+  return tap_fd;
+}
+#else
+int CreateTapInterface() {
+  // Unsupported.
+  return -1;
+}
+#endif  // defined(__linux__)
+
 }  // namespace quic
diff --git a/quiche/quic/masque/masque_utils.h b/quiche/quic/masque/masque_utils.h
index 1743092..68cce05 100644
--- a/quiche/quic/masque/masque_utils.h
+++ b/quiche/quic/masque/masque_utils.h
@@ -21,6 +21,9 @@
 enum : QuicByteCount {
   kMasqueMaxEncapsulatedPacketSize = 1250,
   kMasqueMaxOuterPacketSize = 1350,
+  kMasqueIpPacketBufferSize = 1501,
+  // Enough for a VLAN tag, but not Stacked VLANs.
+  kMasqueEthernetFrameBufferSize = 1523,
 };
 
 // Mode that MASQUE is operating in.
@@ -34,6 +37,10 @@
       1,  // ConnectIp mode uses MASQUE HTTP CONNECT-IP as documented in
   // <https://datatracker.ietf.org/doc/html/draft-ietf-masque-connect-ip>. This
   // mode also allows unauthenticated clients.
+  kConnectEthernet =
+      3,  // ConnectEthernet mode uses MASQUE HTTP CONNECT-ETHERNET.
+  // <https://datatracker.ietf.org/doc/draft-asedeno-masque-connect-ethernet/>
+  // This mode also allows unauthenticated clients.
 };
 
 QUIC_NO_EXPORT std::string MasqueModeToString(MasqueMode masque_mode);
@@ -43,6 +50,9 @@
 // Create a TUN interface, with the specified `client_address`. Requires root.
 int CreateTunInterface(const QuicIpAddress& client_address, bool server = true);
 
+// Create a TAP interface. Requires root.
+int CreateTapInterface();
+
 }  // namespace quic
 
 #endif  // QUICHE_QUIC_MASQUE_MASQUE_UTILS_H_