Skip to content

Commit 630b621

Browse files
authored
Skip TLS and H2 when target is inbound IP (#1219)
In order to prevent the proxy from forwarding inbound traffic to target IP addresses not in the list of inbound ips allowed by a pod, the proxy will be changed to also forward on the original destination rather than the original port on localhost. To support forwarding on the original destination address, an iptables rule that currently results in a traffic loop has to be dropped. The rule allows an app to call itself: when a packet is sent over lo as a network interface and the destination is not localhost, the packet is routed back through the inbound chain (essentially resulting in the following flow: appX -- outbound -- inbound -- appX). The packets sent by the proxy are routed by the kernel through the loopback interface, however, when forwarding on the original destination, the destination address is no longer localhost (resulting in a traffic). Dropping the rule will break the edge case of an app calling itself since the packet will go through the outbound and from outbound straight to the process (packets on loopback skip nat prerouting tables so from outbound /it has/ to be redirected through rules to the inbound; inbound will not pick up the packet) -- the packet will be encrypted by the outbound side and also upgraded to H2 since the outbound does not know the receving end will not be another proxy. This change deals with this edge case: if our target destination is also part of the inbound ips then we will not do any TLS or upgrade the connection to H2. Packet generated by the outbound side of the proxy will now be sent straight to the application process who will be able to deal with it as if it came from the inbound side. To support this change, the list of inbound ips has been wired through the outbound configuration. On each endpoint, we cross check the target against our list of inbound ips -- if the target is an inbound ip, then we don't do any TLS (reason: loopback) and set the protocol hint as Unknown. This is done on both logical and direct connections; skipping TLS should be protocol agnostic. The gateway also had to be changed since it now supports logical and direct connections. Where the configuration is not wired through, we provide defaults (an empty set should still set TLS). N.B. we set this directly on an endpoint for direct communications however iptables currently has a rule which will forward a packet directly to the application process when the address used is the endpoint address (effectively skipping outbound). App X (ep addres) --> App X, App X (logical address) --> outbound --> App X. Signed-off-by: Matei David <[email protected]>
1 parent af31aab commit 630b621

File tree

11 files changed

+101
-15
lines changed

11 files changed

+101
-15
lines changed

linkerd/app/gateway/src/gateway.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ where
6969
// Create an outbound target using the endpoint from the profile.
7070
if let Some((addr, metadata)) = profile.endpoint() {
7171
debug!("Creating outbound endpoint");
72+
// Create empty list of inbound ips, TLS shouldn't be skipped in
73+
// this case.
7274
let svc = self
7375
.outbound
7476
.new_service(svc::Either::B(outbound::http::Endpoint::from((
@@ -78,6 +80,9 @@ where
7880
metadata,
7981
tls::NoClientTls::NotProvidedByServiceDiscovery,
8082
profile.is_opaque_protocol(),
83+
// Address would not be a local IP so always treat
84+
// target as remote in this case.
85+
&Default::default(),
8186
),
8287
))));
8388
return Gateway::new(svc, http.target, local_id);

linkerd/app/gateway/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ where
117117
.push_tcp_endpoint()
118118
.push_tcp_forward()
119119
.into_stack();
120+
let inbound_ips = outbound.config().inbound_ips.clone();
120121
let tcp = endpoint
121122
.push_switch(
122123
move |(profile, _): (Option<profiles::Receiver>, _)| -> Result<_, Error> {
@@ -130,6 +131,7 @@ where
130131
metadata,
131132
tls::NoClientTls::NotProvidedByServiceDiscovery,
132133
profile.is_opaque_protocol(),
134+
&inbound_ips,
133135
)));
134136
}
135137

linkerd/app/outbound/src/endpoint.rs

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ use linkerd_app_core::{
77
transport::{self, addrs::*},
88
transport_header, Conditional,
99
};
10-
use std::{fmt, net::SocketAddr};
10+
use std::{
11+
collections::HashSet,
12+
fmt,
13+
net::{IpAddr, SocketAddr},
14+
sync::Arc,
15+
};
1116

1217
#[derive(Clone, Debug)]
1318
pub struct Endpoint<P> {
@@ -19,9 +24,10 @@ pub struct Endpoint<P> {
1924
pub opaque_protocol: bool,
2025
}
2126

22-
#[derive(Copy, Clone)]
27+
#[derive(Clone)]
2328
pub struct FromMetadata {
2429
pub identity_disabled: bool,
30+
pub inbound_ips: Arc<HashSet<IpAddr>>,
2531
}
2632

2733
// === impl Endpoint ===
@@ -40,13 +46,23 @@ impl Endpoint<()> {
4046

4147
pub fn from_metadata(
4248
addr: impl Into<SocketAddr>,
43-
metadata: Metadata,
49+
mut metadata: Metadata,
4450
reason: tls::NoClientTls,
4551
opaque_protocol: bool,
52+
inbound_ips: &HashSet<IpAddr>,
4653
) -> Self {
54+
let addr: SocketAddr = addr.into();
55+
let tls = if inbound_ips.contains(&addr.ip()) {
56+
metadata.clear_upgrade();
57+
tracing::debug!(%addr, ?metadata, ?addr, ?inbound_ips, "Target is local");
58+
tls::ConditionalClientTls::None(tls::NoClientTls::Loopback)
59+
} else {
60+
FromMetadata::client_tls(&metadata, reason)
61+
};
62+
4763
Self {
48-
addr: Remote(ServerAddr(addr.into())),
49-
tls: FromMetadata::client_tls(&metadata, reason),
64+
addr: Remote(ServerAddr(addr)),
65+
tls,
5066
metadata,
5167
logical_addr: None,
5268
opaque_protocol,
@@ -131,7 +147,6 @@ impl<P: std::hash::Hash> std::hash::Hash for Endpoint<P> {
131147
}
132148

133149
// === EndpointFromMetadata ===
134-
135150
impl FromMetadata {
136151
fn client_tls(metadata: &Metadata, reason: tls::NoClientTls) -> tls::ConditionalClientTls {
137152
// If we're transporting an opaque protocol OR we're communicating with
@@ -166,11 +181,18 @@ impl<P: Copy + std::fmt::Debug> MapEndpoint<Concrete<P>, Metadata> for FromMetad
166181
&self,
167182
concrete: &Concrete<P>,
168183
addr: SocketAddr,
169-
metadata: Metadata,
184+
mut metadata: Metadata,
170185
) -> Self::Out {
171186
tracing::trace!(%addr, ?metadata, ?concrete, "Resolved endpoint");
172-
let tls = if self.identity_disabled {
173-
tls::ConditionalClientTls::None(tls::NoClientTls::Disabled)
187+
let tls = if self.identity_disabled || self.inbound_ips.contains(&addr.ip()) {
188+
let reason = if self.identity_disabled {
189+
tls::NoClientTls::Disabled
190+
} else {
191+
metadata.clear_upgrade();
192+
tracing::debug!(%addr, ?metadata, ?addr, ?self.inbound_ips, "Target is local");
193+
tls::NoClientTls::Loopback
194+
};
195+
tls::ConditionalClientTls::None(reason)
174196
} else {
175197
Self::client_tls(&metadata, tls::NoClientTls::NotProvidedByServiceDiscovery)
176198
};

linkerd/app/outbound/src/http/logical.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,13 @@ impl<E> Outbound<E> {
4545
.check_service::<ConcreteAddr>()
4646
.push_request_filter(|c: Concrete| Ok::<_, Infallible>(c.resolve))
4747
.push(svc::layer::mk(move |inner| {
48-
map_endpoint::Resolve::new(endpoint::FromMetadata { identity_disabled }, inner)
48+
map_endpoint::Resolve::new(
49+
endpoint::FromMetadata {
50+
identity_disabled,
51+
inbound_ips: config.inbound_ips.clone(),
52+
},
53+
inner,
54+
)
4955
}))
5056
.check_service::<Concrete>()
5157
.into_inner();

linkerd/app/outbound/src/lib.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ use linkerd_app_core::{
3030
transport::{self, addrs::*},
3131
AddrMatch, Error, ProxyRuntime,
3232
};
33-
use std::{collections::HashMap, fmt::Debug, time::Duration};
33+
use std::{
34+
collections::{HashMap, HashSet},
35+
fmt::Debug,
36+
net::IpAddr,
37+
sync::Arc,
38+
time::Duration,
39+
};
3440
use tracing::info;
3541

3642
const EWMA_DEFAULT_RTT: Duration = Duration::from_millis(30);
@@ -45,6 +51,7 @@ pub struct Config {
4551
// not perform per-target-address discovery. Non-HTTP connections are
4652
// forwarded without discovery/routing/mTLS.
4753
pub ingress_mode: bool,
54+
pub inbound_ips: Arc<HashSet<IpAddr>>,
4855
}
4956

5057
#[derive(Clone, Debug)]

linkerd/app/outbound/src/switch_logical.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ impl<S> Outbound<S> {
2626
SSvc::Future: Send,
2727
{
2828
let no_tls_reason = self.no_tls_reason();
29-
self.map_stack(|_, _, endpoint| {
29+
self.map_stack(|config, _, endpoint| {
30+
let inbound_ips = config.inbound_ips.clone();
3031
endpoint
3132
.push_switch(
3233
move |(profile, target): (Option<profiles::Receiver>, T)| -> Result<_, Infallible> {
@@ -39,6 +40,7 @@ impl<S> Outbound<S> {
3940
metadata,
4041
no_tls_reason,
4142
rx.is_opaque_protocol(),
43+
&*inbound_ips,
4244
)));
4345
}
4446

linkerd/app/outbound/src/tcp/logical.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,13 @@ where
5353
.check_service::<ConcreteAddr>()
5454
.push_request_filter(|c: Concrete| Ok::<_, Infallible>(c.resolve))
5555
.push(svc::layer::mk(move |inner| {
56-
map_endpoint::Resolve::new(endpoint::FromMetadata { identity_disabled }, inner)
56+
map_endpoint::Resolve::new(
57+
endpoint::FromMetadata {
58+
identity_disabled,
59+
inbound_ips: config.inbound_ips.clone(),
60+
},
61+
inner,
62+
)
5763
}))
5864
.check_service::<Concrete>()
5965
.into_inner();

linkerd/app/outbound/src/tcp/opaque_transport.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ mod test {
160160
metadata,
161161
tls::NoClientTls::NotProvidedByServiceDiscovery,
162162
false,
163+
&Default::default(),
163164
)
164165
}
165166

linkerd/app/outbound/src/test_util.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ pub fn default_config() -> Config {
4343
max_in_flight_requests: 10_000,
4444
detect_protocol_timeout: Duration::from_secs(3),
4545
},
46+
inbound_ips: Default::default(),
4647
}
4748
}
4849

linkerd/app/src/env.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ use inbound::policy;
1212
use std::{
1313
collections::{HashMap, HashSet},
1414
fs,
15-
net::SocketAddr,
15+
net::{IpAddr, SocketAddr},
1616
path::PathBuf,
1717
str::FromStr,
1818
time::Duration,
1919
};
2020
use thiserror::Error;
21-
use tracing::{error, info, warn};
21+
use tracing::{debug, error, info, warn};
2222

2323
/// The strings used to build a configuration.
2424
pub trait Strings {
@@ -68,6 +68,12 @@ pub enum ParseError {
6868
NotANetwork,
6969
#[error("host is not an IP address")]
7070
HostIsNotAnIpAddress,
71+
#[error("not a valid IP address: {0}")]
72+
NotAnIp(
73+
#[from]
74+
#[source]
75+
std::net::AddrParseError,
76+
),
7177
#[error(transparent)]
7278
AddrError(addr::Error),
7379
#[error("not a valid identity name")]
@@ -168,6 +174,8 @@ pub const ENV_POLICY_SVC_BASE: &str = "LINKERD2_PROXY_POLICY_SVC";
168174
pub const ENV_POLICY_WORKLOAD: &str = "LINKERD2_PROXY_POLICY_WORKLOAD";
169175
pub const ENV_POLICY_CLUSTER_NETWORKS: &str = "LINKERD2_PROXY_POLICY_CLUSTER_NETWORKS";
170176

177+
pub const ENV_INBOUND_IPS: &str = "LINKERD2_PROXY_INBOUND_IPS";
178+
171179
pub const ENV_IDENTITY_DISABLED: &str = "LINKERD2_PROXY_IDENTITY_DISABLED";
172180
pub const ENV_IDENTITY_DIR: &str = "LINKERD2_PROXY_IDENTITY_DIR";
173181
pub const ENV_IDENTITY_TRUST_ANCHORS: &str = "LINKERD2_PROXY_IDENTITY_TRUST_ANCHORS";
@@ -390,6 +398,19 @@ pub fn parse_config<S: Strings>(strings: &S) -> Result<super::Config, EnvError>
390398
.unwrap_or_else(|| parse_dns_suffixes(DEFAULT_DESTINATION_PROFILE_SUFFIXES).unwrap());
391399
let dst_profile_networks = dst_profile_networks?.unwrap_or_default();
392400

401+
let inbound_ips = {
402+
let ips = parse(strings, ENV_INBOUND_IPS, parse_ip_set)?.unwrap_or_default();
403+
if ips.is_empty() {
404+
info!(
405+
"`{}` allowlist not configured, allowing all target addresses",
406+
ENV_INBOUND_IPS
407+
);
408+
} else {
409+
debug!(allowed = ?ips, "Only allowing connections targeting `{}`", ENV_INBOUND_IPS);
410+
}
411+
ips.into()
412+
};
413+
393414
let outbound = {
394415
let ingress_mode = parse(strings, ENV_INGRESS_MODE, parse_bool)?.unwrap_or(false);
395416

@@ -441,6 +462,7 @@ pub fn parse_config<S: Strings>(strings: &S) -> Result<super::Config, EnvError>
441462
.unwrap_or(DEFAULT_OUTBOUND_MAX_IN_FLIGHT),
442463
detect_protocol_timeout,
443464
},
465+
inbound_ips,
444466
}
445467
};
446468

@@ -915,6 +937,12 @@ fn parse_socket_addr(s: &str) -> Result<SocketAddr, ParseError> {
915937
}
916938
}
917939

940+
fn parse_ip_set(s: &str) -> Result<HashSet<IpAddr>, ParseError> {
941+
s.split(',')
942+
.map(|s| s.parse::<IpAddr>().map_err(Into::into))
943+
.collect()
944+
}
945+
918946
fn parse_addr(s: &str) -> Result<Addr, ParseError> {
919947
Addr::from_str(s).map_err(|e| {
920948
error!("Not a valid address: {}", s);

0 commit comments

Comments
 (0)