Skip to content

Commit 4dcb4df

Browse files
authored
feat(outbound): Configure TLS routes from the policy API (#3341)
Until now, the TLS routing stack was not wire with the API responses. This PR adds mappings from the protobuf API representation to the correct internal types. This enables the proxy to build the correct sidecar type when it receives a TLS protocol response from the discovery. In addition to that, in a separate commit the TLS rule matching logic has been simplified for our internal types. This can be done because for TLS routes we do not do any real route matching. Furthermore the validation logic in the controller ensures that a TLSRoute has exactly on rule. Signed-off-by: Zahari Dichev <[email protected]>
1 parent 16a3ec2 commit 4dcb4df

File tree

11 files changed

+222
-133
lines changed

11 files changed

+222
-133
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2416,6 +2416,7 @@ version = "0.1.0"
24162416
dependencies = [
24172417
"linkerd-dns",
24182418
"linkerd-tls",
2419+
"linkerd2-proxy-api",
24192420
"rand",
24202421
"regex",
24212422
"thiserror",

linkerd/app/outbound/src/tls/logical/router.rs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -148,15 +148,7 @@ where
148148
.iter()
149149
.map(|route| tls_route::Route {
150150
snis: route.snis.clone(),
151-
rules: route
152-
.rules
153-
.iter()
154-
.cloned()
155-
.map(|tls_route::Rule { matches, policy }| tls_route::Rule {
156-
matches,
157-
policy: mk_policy(policy),
158-
})
159-
.collect(),
151+
policy: mk_policy(route.policy.clone()),
160152
})
161153
.collect();
162154

linkerd/app/outbound/src/tls/logical/tests.rs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -147,26 +147,22 @@ fn default_backend(addr: SocketAddr) -> client_policy::Backend {
147147

148148
fn sni_route(backend: client_policy::Backend, sni: sni::MatchSni) -> client_policy::tls::Route {
149149
use client_policy::{
150-
tls::{Filter, Policy, Route, Rule},
150+
tls::{Filter, Policy, Route},
151151
Meta, RouteBackend, RouteDistribution,
152152
};
153-
use linkerd_tls_route::r#match::MatchSession;
154153
use once_cell::sync::Lazy;
155154
static NO_FILTERS: Lazy<Arc<[Filter]>> = Lazy::new(|| Arc::new([]));
156155
Route {
157156
snis: vec![sni],
158-
rules: vec![Rule {
159-
matches: vec![MatchSession::default()],
160-
policy: Policy {
161-
meta: Meta::new_default("test_route"),
157+
policy: Policy {
158+
meta: Meta::new_default("test_route"),
159+
filters: NO_FILTERS.clone(),
160+
params: (),
161+
distribution: RouteDistribution::FirstAvailable(Arc::new([RouteBackend {
162162
filters: NO_FILTERS.clone(),
163-
params: (),
164-
distribution: RouteDistribution::FirstAvailable(Arc::new([RouteBackend {
165-
filters: NO_FILTERS.clone(),
166-
backend,
167-
}])),
168-
},
169-
}],
163+
backend,
164+
}])),
165+
},
170166
}
171167
}
172168

linkerd/proxy/client-policy/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ publish = false
99
[features]
1010
proto = [
1111
"linkerd-http-route/proto",
12+
"linkerd-tls-route/proto",
1213
"linkerd2-proxy-api",
1314
"prost-types",
1415
"thiserror",

linkerd/proxy/client-policy/src/lib.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,9 @@ pub mod proto {
333333
#[error("invalid opaque route: {0}")]
334334
OpaqueRoute(#[from] opaq::proto::InvalidOpaqueRoute),
335335

336+
#[error("invalid TLS route: {0}")]
337+
TlsRoute(#[from] tls::proto::InvalidTlsRoute),
338+
336339
#[error("invalid backend: {0}")]
337340
Backend(#[from] InvalidBackend),
338341

@@ -479,10 +482,7 @@ pub mod proto {
479482
proxy_protocol::Kind::Http2(http) => Protocol::Http2(http.try_into()?),
480483
proxy_protocol::Kind::Opaque(opaque) => Protocol::Opaque(opaque.try_into()?),
481484
proxy_protocol::Kind::Grpc(grpc) => Protocol::Grpc(grpc.try_into()?),
482-
proxy_protocol::Kind::Tls(_tls) => {
483-
// TODO(ver): impl TryFrom<proxy_protocol::Tls> for `tls::Tls`
484-
return Err(InvalidPolicy::Protocol("TLS not supported yet"));
485-
}
485+
proxy_protocol::Kind::Tls(tls) => Protocol::Tls(tls.try_into()?),
486486
};
487487

488488
let mut backends = BackendSet::default();

linkerd/proxy/client-policy/src/tls.rs

Lines changed: 170 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ pub use linkerd_tls_route::{find, sni, RouteMatch};
55

66
pub type Policy = crate::RoutePolicy<Filter, ()>;
77
pub type Route = tls::Route<Policy>;
8-
pub type Rule = tls::Rule<Policy>;
98

109
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1110
pub struct Tls {
@@ -18,15 +17,12 @@ pub enum Filter {}
1817
pub fn default(distribution: crate::RouteDistribution<Filter>) -> Route {
1918
Route {
2019
snis: vec![],
21-
rules: vec![Rule {
22-
matches: vec![],
23-
policy: Policy {
24-
meta: crate::Meta::new_default("default"),
25-
filters: Arc::new([]),
26-
params: (),
27-
distribution,
28-
},
29-
}],
20+
policy: Policy {
21+
meta: crate::Meta::new_default("default"),
22+
filters: Arc::new([]),
23+
params: (),
24+
distribution,
25+
},
3026
}
3127
}
3228

@@ -39,17 +35,175 @@ impl Default for Tls {
3935
}
4036

4137
#[cfg(feature = "proto")]
42-
pub mod proto {
38+
pub(crate) mod proto {
4339
use super::*;
44-
use crate::proto::BackendSet;
40+
use crate::{
41+
proto::{BackendSet, InvalidBackend, InvalidDistribution, InvalidMeta},
42+
Meta, RouteBackend, RouteDistribution,
43+
};
44+
use linkerd2_proxy_api::outbound::{self, tls_route};
45+
use linkerd_tls_route::sni::proto::InvalidSniMatch;
46+
47+
use once_cell::sync::Lazy;
48+
use std::sync::Arc;
49+
50+
pub(crate) static NO_FILTERS: Lazy<Arc<[Filter]>> = Lazy::new(|| Arc::new([]));
51+
52+
#[derive(Debug, thiserror::Error)]
53+
pub enum InvalidTlsRoute {
54+
#[error("invalid sni match: {0}")]
55+
SniMatch(#[from] InvalidSniMatch),
56+
57+
#[error("invalid route metadata: {0}")]
58+
Meta(#[from] InvalidMeta),
59+
60+
#[error("invalid distribution: {0}")]
61+
Distribution(#[from] InvalidDistribution),
62+
63+
/// Note: this restriction may be removed in the future, if a way of
64+
/// actually matching rules for TLS routes is added.
65+
#[error("a TLS route must have exactly one rule, but {0} were provided")]
66+
OnlyOneRule(usize),
67+
68+
#[error("no filters can be configured on opaque routes yet")]
69+
NoFilters,
70+
71+
#[error("missing {0}")]
72+
Missing(&'static str),
73+
}
74+
75+
impl TryFrom<outbound::proxy_protocol::Tls> for Tls {
76+
type Error = InvalidTlsRoute;
77+
fn try_from(proto: outbound::proxy_protocol::Tls) -> Result<Self, Self::Error> {
78+
let routes = proto
79+
.routes
80+
.into_iter()
81+
.map(try_route)
82+
.collect::<Result<Arc<[_]>, _>>()?;
83+
84+
Ok(Self { routes })
85+
}
86+
}
4587

4688
impl Tls {
4789
pub fn fill_backends(&self, set: &mut BackendSet) {
48-
for Route { ref rules, .. } in &*self.routes {
49-
for Rule { ref policy, .. } in rules {
50-
policy.distribution.fill_backends(set);
51-
}
90+
for Route { ref policy, .. } in &*self.routes {
91+
policy.distribution.fill_backends(set);
5292
}
5393
}
5494
}
95+
96+
fn try_route(proto: outbound::TlsRoute) -> Result<Route, InvalidTlsRoute> {
97+
let outbound::TlsRoute {
98+
rules,
99+
snis,
100+
metadata,
101+
..
102+
} = proto;
103+
let meta = Arc::new(
104+
metadata
105+
.ok_or(InvalidMeta("missing metadata"))?
106+
.try_into()?,
107+
);
108+
109+
let snis = snis
110+
.into_iter()
111+
.map(sni::MatchSni::try_from)
112+
.collect::<Result<Vec<_>, _>>()?;
113+
114+
if rules.len() != 1 {
115+
// Currently, TLS rules have no match expressions, so if there's
116+
// more than one rule, we have no way of determining which one to
117+
// use. Therefore, require that there's exactly one rule.
118+
return Err(InvalidTlsRoute::OnlyOneRule(rules.len()));
119+
}
120+
121+
let policy = rules
122+
.into_iter()
123+
.map(|rule| try_rule(&meta, rule))
124+
.next()
125+
.ok_or(InvalidTlsRoute::OnlyOneRule(0))??;
126+
127+
Ok(Route { snis, policy })
128+
}
129+
130+
fn try_rule(
131+
meta: &Arc<Meta>,
132+
tls_route::Rule { backends }: tls_route::Rule,
133+
) -> Result<Policy, InvalidTlsRoute> {
134+
let distribution = backends
135+
.ok_or(InvalidTlsRoute::Missing("distribution"))?
136+
.try_into()?;
137+
138+
Ok(Policy {
139+
meta: meta.clone(),
140+
filters: NO_FILTERS.clone(),
141+
params: (),
142+
distribution,
143+
})
144+
}
145+
146+
impl TryFrom<tls_route::Distribution> for RouteDistribution<Filter> {
147+
type Error = InvalidDistribution;
148+
fn try_from(distribution: tls_route::Distribution) -> Result<Self, Self::Error> {
149+
use tls_route::{distribution, WeightedRouteBackend};
150+
151+
Ok(
152+
match distribution.kind.ok_or(InvalidDistribution::Missing)? {
153+
distribution::Kind::Empty(_) => RouteDistribution::Empty,
154+
distribution::Kind::RandomAvailable(distribution::RandomAvailable {
155+
backends,
156+
}) => {
157+
let backends = backends
158+
.into_iter()
159+
.map(|WeightedRouteBackend { weight, backend }| {
160+
let backend = backend
161+
.ok_or(InvalidDistribution::MissingBackend)?
162+
.try_into()?;
163+
Ok((backend, weight))
164+
})
165+
.collect::<Result<Arc<[_]>, InvalidDistribution>>()?;
166+
if backends.is_empty() {
167+
return Err(InvalidDistribution::Empty("RandomAvailable"));
168+
}
169+
RouteDistribution::RandomAvailable(backends)
170+
}
171+
distribution::Kind::FirstAvailable(distribution::FirstAvailable {
172+
backends,
173+
}) => {
174+
let backends = backends
175+
.into_iter()
176+
.map(RouteBackend::try_from)
177+
.collect::<Result<Arc<[_]>, InvalidBackend>>()?;
178+
if backends.is_empty() {
179+
return Err(InvalidDistribution::Empty("FirstAvailable"));
180+
}
181+
RouteDistribution::FirstAvailable(backends)
182+
}
183+
},
184+
)
185+
}
186+
}
187+
188+
impl TryFrom<tls_route::RouteBackend> for RouteBackend<Filter> {
189+
type Error = InvalidBackend;
190+
fn try_from(
191+
tls_route::RouteBackend {
192+
backend,
193+
invalid: _, // TODO
194+
}: tls_route::RouteBackend,
195+
) -> Result<Self, Self::Error> {
196+
let backend = backend.ok_or(InvalidBackend::Missing("backend"))?;
197+
RouteBackend::try_from_proto(backend, std::iter::empty::<()>())
198+
}
199+
}
200+
201+
// Necessary to satisfy `RouteBackend::try_from_proto` type constraints.
202+
// TODO(eliza): if filters are added to opaque routes, change this to a
203+
// proper `TryFrom` impl...
204+
impl From<()> for Filter {
205+
fn from(_: ()) -> Self {
206+
unreachable!("no filters can be configured on opaque routes yet")
207+
}
208+
}
55209
}

linkerd/tls/route/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,18 @@ license = "Apache-2.0"
55
edition = "2021"
66
publish = false
77

8+
[features]
9+
proto = ["linkerd2-proxy-api"]
10+
811
[dependencies]
912
regex = "1"
1013
rand = "0.8"
1114
thiserror = "1"
1215
tracing = "0.1"
1316
linkerd-tls = { path = "../" }
1417
linkerd-dns = { path = "../../dns" }
18+
19+
[dependencies.linkerd2-proxy-api]
20+
workspace = true
21+
optional = true
22+
features = ["outbound"]

linkerd/tls/route/src/lib.rs

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@
55
#![forbid(unsafe_code)]
66

77
use linkerd_tls::ServerName;
8-
use r#match::SessionMatch;
98
use tracing::trace;
109

11-
pub mod r#match;
1210
pub mod sni;
1311
#[cfg(test)]
1412
mod tests;
@@ -24,19 +22,6 @@ pub struct Route<P> {
2422
/// When no SNI matches are present, all SNIs match.
2523
pub snis: Vec<MatchSni>,
2624

27-
/// Must not be empty.
28-
pub rules: Vec<Rule<P>>,
29-
}
30-
31-
/// Policies for a given set of route matches.
32-
#[derive(Clone, Debug, Default, Hash, PartialEq, Eq)]
33-
pub struct Rule<P> {
34-
/// A list of session matchers, *any* of which may apply.
35-
///
36-
/// The "best" match is used when comparing rules.
37-
pub matches: Vec<r#match::MatchSession>,
38-
39-
/// The policy to apply to sessions matched by this rule.
4025
pub policy: P,
4126
}
4227

@@ -45,7 +30,6 @@ pub struct Rule<P> {
4530
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
4631
pub struct RouteMatch {
4732
sni: Option<SniMatch>,
48-
route: r#match::SessionMatch,
4933
}
5034

5135
/// Provides metadata information about a TLS session. For now this contains
@@ -74,27 +58,7 @@ pub fn find<P>(routes: &[Route<P>], session_info: SessionInfo) -> Option<(RouteM
7458
Some(sni_match)
7559
};
7660

77-
trace!(rules = %rt.rules.len());
78-
let (route, policy) = best(rt.rules.iter().filter_map(|rule| {
79-
// If there are no matches in the list, then the rule has an
80-
// implicit default match.
81-
if rule.matches.is_empty() {
82-
trace!("implicit match");
83-
return Some((SessionMatch::default(), &rule.policy));
84-
}
85-
// Find the best match to compare against other rules/routes
86-
// (if any apply). The order/precedence of matches is not
87-
// relevant.
88-
let summary = rule
89-
.matches
90-
.iter()
91-
.filter_map(|m| m.match_session(&session_info))
92-
.max()?;
93-
trace!("matches!");
94-
Some((summary, &rule.policy))
95-
}))?;
96-
97-
Some((RouteMatch { sni, route }, policy))
61+
Some((RouteMatch { sni }, &rt.policy))
9862
}))
9963
}
10064

0 commit comments

Comments
 (0)