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_