Skip to content

Commit b230155

Browse files
authored
inbound: Improve policy-based protocol detection (#1258)
When an inbound server is configured with `proxyProtocol: HTTP/1`, we have to do detection since client proxies may connect to us over HTTP/2. But if the client is unmeshed or if detection fails, we can assume the connection wasn't H2 upgraded. This change modifies the inbound detection stack to use this improved default behavior.
1 parent e363a65 commit b230155

File tree

2 files changed

+44
-21
lines changed

2 files changed

+44
-21
lines changed

linkerd/app/inbound/src/detect.rs

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use linkerd_app_core::{
1414
Error, Infallible,
1515
};
1616
use std::{fmt::Debug, time};
17+
use tracing::info;
1718

1819
#[derive(Clone, Debug, PartialEq, Eq)]
1920
pub(crate) struct Forward {
@@ -106,7 +107,6 @@ impl<N> Inbound<N> {
106107

107108
let detect_timeout = cfg.proxy.detect_protocol_timeout;
108109
detect
109-
.check_new_service::<Tls, _>()
110110
.push_switch(
111111
// Ensure that the connection is authorized before proceeding with protocol
112112
// detection.
@@ -135,12 +135,10 @@ impl<N> Inbound<N> {
135135
.push_on_service(svc::MapTargetLayer::new(io::BoxedIo::new))
136136
.into_inner(),
137137
)
138-
.check_new_service::<(tls::ConditionalServerTls, T), _>()
139138
.push(tls::NewDetectTls::layer(TlsParams {
140139
timeout: tls::server::Timeout(detect_timeout),
141140
identity: rt.identity.clone(),
142141
}))
143-
.check_new_service::<T, I>()
144142
.push_switch(
145143
// If this port's policy indicates that authentication is not required and
146144
// detection should be skipped, use the TCP stack directly.
@@ -162,7 +160,6 @@ impl<N> Inbound<N> {
162160
.push_on_service(svc::MapTargetLayer::new(io::BoxedIo::new))
163161
.into_inner(),
164162
)
165-
.check_new_service::<T, I>()
166163
.push_on_service(svc::BoxService::layer())
167164
.push(svc::ArcNewService::layer())
168165
})
@@ -193,14 +190,39 @@ impl<N> Inbound<N> {
193190
.push(transport::metrics::NewServer::layer(
194191
rt.metrics.proxy.transport.clone(),
195192
))
196-
.check_new_service::<Http, io::PrefixedIo<I>>()
197193
.push_switch(
198-
|(http, Detect { tls, .. })| -> Result<_, Infallible> {
199-
match http {
200-
Some(http) => Ok(svc::Either::A(Http { http, tls })),
194+
|(detected, Detect { tls, .. })| -> Result<_, Infallible> {
195+
match detected {
196+
Ok(Some(http)) => Ok(svc::Either::A(Http { http, tls })),
197+
Ok(None) => Ok(svc::Either::B(tls)),
201198
// When HTTP detection fails, forward the connection to the application as
202199
// an opaque TCP stream.
203-
None => Ok(svc::Either::B(tls)),
200+
Err(timeout) => match tls.policy.protocol() {
201+
Protocol::Http1 => {
202+
// If the protocol was hinted to be HTTP/1.1 but detection
203+
// failed, we'll usually be handling HTTP/1, but we may actually
204+
// be handling HTTP/2 via protocol upgrade. Our options are:
205+
// handle the connection as HTTP/1, assuming it will be rare for
206+
// a proxy to initiate TLS, etc and not send the 16B of
207+
// connection header; or we can handle it as opaque--but there's
208+
// no chance the server will be able to handle the H2 protocol
209+
// upgrade. So, it seems best to assume it's HTTP/1 and let the
210+
// proxy handle the protocol error if we're in an edge case.
211+
info!(%timeout, "Handling connection as HTTP/1 due to policy");
212+
Ok(svc::Either::A(Http {
213+
http: http::Version::Http1,
214+
tls,
215+
}))
216+
}
217+
// Otherwise, the protocol hint must have been `Detect` or the
218+
// protocol was updated after detection was initiated, otherwise we
219+
// would have avoided detection below. Continue handling the
220+
// connection as if it were opaque.
221+
_ => {
222+
info!(%timeout, "Handling connection as opaque");
223+
Ok(svc::Either::B(tls))
224+
}
225+
},
204226
}
205227
},
206228
svc::stack(forward)
@@ -212,16 +234,12 @@ impl<N> Inbound<N> {
212234
.push(policy::NewAuthorizeTcp::layer(rt.metrics.tcp_authz.clone()))
213235
.into_inner(),
214236
)
215-
.push(svc::ArcNewService::layer())
216-
.push_map_target(detect::allow_timeout)
217-
.push(detect::NewDetectService::layer(ConfigureHttpDetect))
218-
.check_new_service::<Detect, I>();
237+
.push(detect::NewDetectService::layer(ConfigureHttpDetect));
219238

220239
http.push_on_service(svc::MapTargetLayer::new(io::BoxedIo::new))
221240
.push(transport::metrics::NewServer::layer(
222241
rt.metrics.proxy.transport.clone(),
223242
))
224-
.check_new_service::<Http, I>()
225243
.push_switch(
226244
// If we have a protocol hint, skip detection and just used the hinted HTTP
227245
// version.
@@ -230,14 +248,20 @@ impl<N> Inbound<N> {
230248
Protocol::Detect { timeout } => {
231249
return Ok(svc::Either::B(Detect { timeout, tls }));
232250
}
233-
Protocol::Http1 => {
234-
// HTTP/1 services may actually be transported over HTTP/2
235-
// connections between proxies, so we have to do detection.
251+
// Meshed HTTP/1 services may actually be transported over HTTP/2 connections
252+
// between proxies, so we have to do detection.
253+
//
254+
// TODO(ver) outbound clients should hint this with ALPN so we don't
255+
// have to detect this situation.
256+
Protocol::Http1 if tls.status.is_some() => {
236257
return Ok(svc::Either::B(Detect {
237258
timeout: detect_timeout,
238259
tls,
239260
}));
240261
}
262+
// Unmeshed services don't use protocol upgrading, so we can use the
263+
// hint without further detection.
264+
Protocol::Http1 => http::Version::Http1,
241265
Protocol::Http2 | Protocol::Grpc => http::Version::H2,
242266
_ => unreachable!("opaque protocols must not hit the HTTP stack"),
243267
};
@@ -247,7 +271,6 @@ impl<N> Inbound<N> {
247271
)
248272
.push_on_service(svc::BoxService::layer())
249273
.push(svc::ArcNewService::layer())
250-
.check_new_service::<Tls, I>()
251274
})
252275
}
253276
}

linkerd/app/inbound/src/http/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,9 @@ pub mod fuzz {
216216
authorizations: vec![policy::Authorization {
217217
authentication: policy::Authentication::Unauthenticated,
218218
networks: vec![std::net::IpAddr::from([192, 0, 2, 3]).into()],
219-
name: "testsaz".to_string(),
219+
name: "testsaz".into(),
220220
}],
221-
name: "testsrv".to_string(),
221+
name: "testsrv".into(),
222222
},
223223
);
224224
policy
@@ -227,7 +227,7 @@ pub mod fuzz {
227227

228228
impl svc::Param<policy::ServerLabel> for Target {
229229
fn param(&self) -> policy::ServerLabel {
230-
policy::ServerLabel("testsrv".to_string())
230+
policy::ServerLabel("testsrv".into())
231231
}
232232
}
233233

0 commit comments

Comments
 (0)