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_