quiche: implement QUIC specific TransportSocketFactory for TLS context#8091
quiche: implement QUIC specific TransportSocketFactory for TLS context#8091mattklein123 merged 25 commits intoenvoyproxy:masterfrom
Conversation
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
|
/assign @alyssawilk |
Signed-off-by: Dan Zhang <danzh@google.com>
lambdai
left a comment
There was a problem hiding this comment.
It seems the quic filter chain is quite different...
| } else { | ||
| transport_socket.set_name( | ||
| Extensions::TransportSockets::TransportSocketNames::get().RawBuffer); | ||
| std::vector<std::string> server_names(filter_chain.filter_chain_match().server_names().begin(), |
There was a problem hiding this comment.
nit: move into if (!is_quic_)
| ListenerFilterChainFactoryBuilder( | ||
| ListenerImpl& listener, Configuration::TransportSocketFactoryContextImpl& factory_context); | ||
| ListenerImpl& listener, Configuration::TransportSocketFactoryContextImpl& factory_context, | ||
| bool is_quic); |
There was a problem hiding this comment.
My impression is that TCPListenerFilterChainFactoryBuilder and QUIC one has nothing shared... Why not derived QUIC one from FilterChainFactoryBuilder?
There was a problem hiding this comment.
both of them access ListenerImpl and TransportSocketFactoryContextImpl. I made QuicListenerFilterChainFactoryBuilder a stand alone class deriving from ListenerFilterChainFactoryBuilder.
| addListenSocketOptions(Network::SocketOptionFactory::buildRxQueueOverFlowOptions()); | ||
| std::string listener_name = | ||
| config.has_udp_listener_config() ? config.udp_listener_config().udp_listener_name() : ""; | ||
| is_quic = listener_name == UdpListenerNames::get().Quic; |
There was a problem hiding this comment.
nit: is_quic = config.has_udp_listener_config() && config.udp_listener_config().udp_listener_name() == UdpListenerNames::get().Quic;
Also you can create const ref of udp_listener_config here and declare listener_name const
const auto & udp_config = config.has_udp_listener_config() ? config.udp_listener_config() : envoy::api::v2::listener::UdpListenerConfig();
const auto listener_name = udp_config.udp_listener_name().empty() ? udp_config.udp_listener_name() : "";
The idea is to figure out the final listener_name at one place
| // Network::FilterChain | ||
| const Network::TransportSocketFactory* transportSocketFactory() const override { return nullptr; } | ||
|
|
||
| Ssl::ContextConfig* tlsContextConfig() const { return tls_context_config_.get(); } |
There was a problem hiding this comment.
not used yet. Will be used to create EnvoyQuicProofSource which is a fake one now.
There was a problem hiding this comment.
Can this return a const reference or just a reference?
|
@danzh2010 friendly request to please update the PR description to something more descriptive. I will take a look when the review is a bit further along. Thank you! /wait |
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
|
/retest |
|
🔨 rebuilding |
|
I updated PR description. It's good for another around of review. |
lambdai
left a comment
There was a problem hiding this comment.
Thank you for the update!
|
/assign @goaway |
| * @return a transport socket factory to be used by the new connection. | ||
| * nullptr if the connection doesn't need a transport socket. | ||
| */ | ||
| virtual const TransportSocketFactory& transportSocketFactory() const PURE; |
There was a problem hiding this comment.
I think envoy standard might be using optional, but I'm a fan of pointers so we'll see what Matt says ;-)
There was a problem hiding this comment.
an optional reference wrapper is "nicer" but I'm fine either way.
| ASSERT(socket_type == Network::Address::SocketType::Datagram, | ||
| "Only datagram/stream listener supported"); | ||
| RELEASE_ASSERT(config.udpListenerFactory() != nullptr, | ||
| "UDP listner factory is not initialized."); |
There was a problem hiding this comment.
Can you add a TODO to make this a proper config check?
There was a problem hiding this comment.
This won't be a config error, we will create UdpListenerFactory anyway in ListenerImpl(). If not specified in config, we create the raw UDP listener factory.
There was a problem hiding this comment.
I think you can remove this assert then? It would crash in an obvious way on the next line.
There was a problem hiding this comment.
I think you didn't remove it, just converted it. Would it actually fail under createActiveUdpListener or is this assert required?
There was a problem hiding this comment.
ListenerConfig.udpListenerFactory() is populated to default RawUdp listener if not specified in config. So this ASSERT is to check we actually have logic to do this before reaching here. Otherwise below config.udpListenerFactory()->createActiveUdpListener(*this, dispatcher_, logger_, config); would crash because of accessing members of nullptr.
There was a problem hiding this comment.
Gotcha. If we plan to keep it in, let's at least change
listner -> listener
| if (result.rc_ >= 0) { | ||
| RELEASE_ASSERT(os_sys_calls.close(result.rc_).rc_ == 0, ""); | ||
| RELEASE_ASSERT(os_sys_calls.close(result.rc_).rc_ == 0, | ||
| absl::StrCat("fail to close fd ", result.rc_)); |
There was a problem hiding this comment.
Thanks for adding details here.
maybe -> "failed to close fd: response code " ?
since right now it looks like you're printing the fd, not the error code.
| parent_.server_.random(), parent_.server_.stats(), parent_.server_.singletonManager(), | ||
| parent_.server_.threadLocal(), validation_visitor, parent_.server_.api()); | ||
| factory_context.setInitManager(initManager()); | ||
| ListenerFilterChainFactoryBuilderPtr builder = |
There was a problem hiding this comment.
if I'm not lost in braces, we'll now do work for raw UDP that we didn't used to. Is that correct, and is it intentional?
There was a problem hiding this comment.
Yes, we are initializing network filters for raw UDP too. I think it doesn't hurt as long as they don't config any network filters but leave them unused.
mattklein123
left a comment
There was a problem hiding this comment.
Flushing out a few questions/comments from a first pass. Thank you!
/wait
| * @return a transport socket factory to be used by the new connection. | ||
| * nullptr if the connection doesn't need a transport socket. | ||
| */ | ||
| virtual const TransportSocketFactory& transportSocketFactory() const PURE; |
There was a problem hiding this comment.
an optional reference wrapper is "nicer" but I'm fine either way.
include/envoy/ssl/context_config.h
Outdated
|
|
||
| virtual ContextConfigPtr | ||
| createSslContextConfig(const Protobuf::Message& config, | ||
| Server::Configuration::TransportSocketFactoryContext& context) PURE; |
There was a problem hiding this comment.
It's a little strange to create a transport socket factory context when making an SSL context. Should we just define whatever context we need in this file and then have TransportSocketFactoryContext derive from that if needed?
| namespace Tls { | ||
|
|
||
| /** | ||
| * A factory to create server side Tls context config from a protobuf. |
There was a problem hiding this comment.
nit: Tls/TLS here and elsewhere in comments.
| const Protobuf::Message& config, | ||
| Server::Configuration::TransportSocketFactoryContext& context) { | ||
| return std::make_unique<ServerContextConfigImpl>( | ||
| dynamic_cast<const envoy::api::v2::auth::DownstreamTlsContext&>(config), context); |
There was a problem hiding this comment.
If we universally do a dynamic_cast here why not just pass this type as a parameter?
There was a problem hiding this comment.
createSslContextConfig() is public interface which will also be used on client side which should downcast message to UpstreamTlsContext.
There was a problem hiding this comment.
@lizan any thoughts on this one? I would need to sit down and page in all of this code but something seems off here. Maybe this is related to your other comment.
There was a problem hiding this comment.
at interface level this seems fine, though at higher level I think this should just be a TransportSocketConfigFactory
| ASSERT(socket_type == Network::Address::SocketType::Datagram, | ||
| "Only datagram/stream listener supported"); | ||
| RELEASE_ASSERT(config.udpListenerFactory() != nullptr, | ||
| "UDP listner factory is not initialized."); |
There was a problem hiding this comment.
Can you add a TODO to make this a proper config check?
| // Network::FilterChain | ||
| const Network::TransportSocketFactory* transportSocketFactory() const override { return nullptr; } | ||
|
|
||
| Ssl::ContextConfig* tlsContextConfig() const { return tls_context_config_.get(); } |
There was a problem hiding this comment.
Can this return a const reference or just a reference?
| .createActiveUdpListenerFactory(config.has_udp_listener_config() | ||
| ? config.udp_listener_config() | ||
| : envoy::api::v2::listener::UdpListenerConfig()); | ||
| } catch (EnvoyException ex) { |
| ? config.udp_listener_config() | ||
| : envoy::api::v2::listener::UdpListenerConfig()); | ||
| } catch (EnvoyException ex) { | ||
| // TODO(danzh): add implementation for quic_listener and do not catch |
There was a problem hiding this comment.
I'm confused. Why do we need/want this fallback?
There was a problem hiding this comment.
Just because we don't have QuicListenerFactoryConfig registered yet. Once PR #7896 gets in, this is not needed.
There was a problem hiding this comment.
Can we just assert this then or wait until the other one is merged?
There was a problem hiding this comment.
I'll wait for the other PR merge first and change this line. It is as such now to make listener mgr test pass in a meaningful way.
| const ::envoy::api::v2::listener::FilterChain& filter_chain) const { | ||
| // Initialize filter chain for QUIC. | ||
| if (!filter_chain.has_tls_context()) { | ||
| return nullptr; |
There was a problem hiding this comment.
Should this be a config error?
There was a problem hiding this comment.
Yes. Is it okay to lead to connection close? Or what's the best way of handling such error?
There was a problem hiding this comment.
We should check this during config load and fail the config, it should be an assert at this point IMO.
There was a problem hiding this comment.
With removal of QuicFilterChain, this builder can be unified with tcp one too. This check no longer apply.
| return nullptr; | ||
| } | ||
| auto& tls_context_factory = | ||
| Config::Utility::getAndCheckFactory<Ssl::ContextConfigFactory>("downstream_tls_context"); |
There was a problem hiding this comment.
"downstream_tls_context" should be a constant somewhere.
Can you talk a bit more about why we need to load the factory on this way vs. just directly instantiating it? It's a little confusing. I suppose this is due to pluggable TLS implementations?
There was a problem hiding this comment.
Yes, all the registered class work in this PR is just to make envoy core code not depends on TLS extensions.
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
| createActiveUdpListener(ConnectionHandler& parent, Event::Dispatcher& disptacher, | ||
| spdlog::logger& logger, Network::ListenerConfig& config) const PURE; | ||
|
|
||
| // @return true if the protocol above UDP doesn't form a connection. |
There was a problem hiding this comment.
super nitty but /** .. */ style comments
Also maybe
"if the protocol above UDP doesn't form a connection." -> "If the UDP passing through listener doesn't form stateful connections" ?
might even be nicer to invert and have isStatefulConnection() but maybe that's just me.
| ASSERT(socket_type == Network::Address::SocketType::Datagram, | ||
| "Only datagram/stream listener supported"); | ||
| RELEASE_ASSERT(config.udpListenerFactory() != nullptr, | ||
| "UDP listner factory is not initialized."); |
There was a problem hiding this comment.
I think you didn't remove it, just converted it. Would it actually fail under createActiveUdpListener or is this assert required?
|
Let's merge master and then we can get back to reviewing this one. /wait |
Signed-off-by: Dan Zhang <danzh@google.com>
Done. PTAL |
Signed-off-by: Dan Zhang <danzh@google.com>
alyssawilk
left a comment
There was a problem hiding this comment.
Looking good. just one nit left on my end.
| ASSERT(socket_type == Network::Address::SocketType::Datagram, | ||
| "Only datagram/stream listener supported"); | ||
| RELEASE_ASSERT(config.udpListenerFactory() != nullptr, | ||
| "UDP listner factory is not initialized."); |
There was a problem hiding this comment.
Gotcha. If we plan to keep it in, let's at least change
listner -> listener
mattklein123
left a comment
There was a problem hiding this comment.
Thanks, LGTM with some small comments.
/wait
| // server and client. | ||
| class QuicTransportSocketFactoryBase : public Network::TransportSocketFactory { | ||
| public: | ||
| ~QuicTransportSocketFactoryBase() override = default; |
| Ssl::ServerContextConfig& serverContextConfig() const { return *config_; } | ||
|
|
||
| private: | ||
| Ssl::ServerContextConfigPtr config_; |
| class QuicTransportSocketConfigFactory | ||
| : public virtual Server::Configuration::TransportSocketConfigFactory { | ||
| public: | ||
| ~QuicTransportSocketConfigFactory() override = default; |
| } else { | ||
| ASSERT(socket_type == Network::Address::SocketType::Datagram, | ||
| "Only datagram/stream listener supported"); | ||
| RELEASE_ASSERT(config.udpListenerFactory() != nullptr, |
There was a problem hiding this comment.
The previous conversation was lost, but this should never happen due to being checked via config elsewhere, right? If so can we make this a normal ASSERT?
There was a problem hiding this comment.
Early on ListenerImpl() will always create some sort of ActiveUdpListener, if not configured, ActiveRawUdpListener will be created. So yes here ASSERT is sufficient.
| } else if (udp_listener_factory_ != nullptr && | ||
| !udp_listener_factory_->isTransportConnectionless()) { | ||
| for (auto& filter_chain : config.filter_chains()) { | ||
| // Early fail if any filter chain doesn't have transport socket configured. |
There was a problem hiding this comment.
I'm a little confused by this. If the underlying filter chain builder implementation requires a transport socket (like for QUIC) can't it just fail in that code? IMO that would be more clear? WDYT?
There was a problem hiding this comment.
filter chain builder doesn't know if this listener is UDP or TCP, nor does it know about if it's connectionless or not. For non-Quic listener, it's empty transport_socket is allowed, and in that case a factory for RawBufferSocket will be created..
source/server/well_known_names.h
Outdated
| class UdpListenerNameValues { | ||
| public: | ||
| const std::string RawUdp = "raw_udp_listener"; | ||
| const std::string Quic = "quic_listener"; |
alyssawilk
left a comment
There was a problem hiding this comment.
LGTM, modulo Matt's comments
|
Ping? |
mattklein123
left a comment
There was a problem hiding this comment.
Let's ship and iterate! Well done. :)
envoyproxy#8091) Signed-off-by: Dan Zhang <danzh@google.com>
envoyproxy#8091) Signed-off-by: Dan Zhang <danzh@google.com>
envoyproxy#8091) Signed-off-by: Dan Zhang <danzh@google.com>
Implement TransportSocketFactory for QUIC which carries Ssl::ContextConfig. The Ssl::ContextConfig instance is created based on envoy::api::v2::auth::DownstreamTlsContext in envoy::api::v2::listener::FilterChain::transport_socket.
QuicServer|ClientTransportSocketFactory has new interface server|clientContextConfig() to provide access toTLS context in addition to inherited TransportSocketFactory interface. QuicTransportSocketFactory::createTransportSocket() shouldn't be called in current implementation because QUICHE stack already does all the transport layer work.
When listener config socket type is Datagram and udp_listener_config.udp_listener_name is "quiche_quic_listener", envoy::api::v2::listener::FilterChain::transport_socket has to be configured with envoy::api::v2::auth::DownstreamTlsContext or envoy::api::v2::auth::UpstreamTlsContext.
ListenerImpl() will create a QuicTransportSocketFactory instance as part of filter chain in the same way as creating other transport socket factories.
Risk Level: medium, a bad config can hit RELEASE_ASSERT.
Testing: added unit tests for ListenerManagerImpl
Part of #2557