Line data Source code
1 : /// A stand-alone program that routes connections, e.g. from
2 : /// `aaa--bbb--1234.external.domain` to `aaa.bbb.internal.domain:1234`.
3 : ///
4 : /// This allows connecting to pods/services running in the same Kubernetes cluster from
5 : /// the outside. Similar to an ingress controller for HTTPS.
6 : use std::{net::SocketAddr, sync::Arc};
7 :
8 : use futures::future::Either;
9 : use itertools::Itertools;
10 : use proxy::config::TlsServerEndPoint;
11 : use proxy::context::RequestMonitoring;
12 : use proxy::proxy::run_until_cancelled;
13 : use tokio::net::TcpListener;
14 :
15 : use anyhow::{anyhow, bail, ensure, Context};
16 : use clap::{self, Arg};
17 : use futures::TryFutureExt;
18 : use proxy::console::messages::MetricsAuxInfo;
19 : use proxy::stream::{PqStream, Stream};
20 :
21 : use tokio::io::{AsyncRead, AsyncWrite};
22 : use tokio_util::sync::CancellationToken;
23 : use utils::{project_git_version, sentry_init::init_sentry};
24 :
25 : use tracing::{error, info, Instrument};
26 :
27 : project_git_version!(GIT_VERSION);
28 :
29 1 : fn cli() -> clap::Command {
30 1 : clap::Command::new("Neon proxy/router")
31 1 : .version(GIT_VERSION)
32 1 : .arg(
33 1 : Arg::new("listen")
34 1 : .short('l')
35 1 : .long("listen")
36 1 : .help("listen for incoming client connections on ip:port")
37 1 : .default_value("127.0.0.1:4432"),
38 1 : )
39 1 : .arg(
40 1 : Arg::new("tls-key")
41 1 : .short('k')
42 1 : .long("tls-key")
43 1 : .help("path to TLS key for client postgres connections")
44 1 : .required(true),
45 1 : )
46 1 : .arg(
47 1 : Arg::new("tls-cert")
48 1 : .short('c')
49 1 : .long("tls-cert")
50 1 : .help("path to TLS cert for client postgres connections")
51 1 : .required(true),
52 1 : )
53 1 : .arg(
54 1 : Arg::new("dest")
55 1 : .short('d')
56 1 : .long("destination")
57 1 : .help("append this domain zone to the SNI hostname to get the destination address")
58 1 : .required(true),
59 1 : )
60 1 : }
61 :
62 : #[tokio::main]
63 1 : async fn main() -> anyhow::Result<()> {
64 1 : let _logging_guard = proxy::logging::init().await?;
65 1 : let _panic_hook_guard = utils::logging::replace_panic_hook_with_tracing_panic_hook();
66 1 : let _sentry_guard = init_sentry(Some(GIT_VERSION.into()), &[]);
67 1 :
68 1 : let args = cli().get_matches();
69 1 : let destination: String = args.get_one::<String>("dest").unwrap().parse()?;
70 1 :
71 1 : // Configure TLS
72 1 : let (tls_config, tls_server_end_point): (Arc<rustls::ServerConfig>, TlsServerEndPoint) = match (
73 1 : args.get_one::<String>("tls-key"),
74 1 : args.get_one::<String>("tls-cert"),
75 1 : ) {
76 1 : (Some(key_path), Some(cert_path)) => {
77 1 : let key = {
78 1 : let key_bytes = std::fs::read(key_path).context("TLS key file")?;
79 1 : let mut keys = rustls_pemfile::pkcs8_private_keys(&mut &key_bytes[..])
80 1 : .context(format!("Failed to read TLS keys at '{key_path}'"))?;
81 1 :
82 1 : ensure!(keys.len() == 1, "keys.len() = {} (should be 1)", keys.len());
83 1 : keys.pop().map(rustls::PrivateKey).unwrap()
84 1 : };
85 1 :
86 1 : let cert_chain_bytes = std::fs::read(cert_path)
87 1 : .context(format!("Failed to read TLS cert file at '{cert_path}.'"))?;
88 1 :
89 1 : let cert_chain = {
90 1 : rustls_pemfile::certs(&mut &cert_chain_bytes[..])
91 1 : .context(format!(
92 1 : "Failed to read TLS certificate chain from bytes from file at '{cert_path}'."
93 1 : ))?
94 1 : .into_iter()
95 1 : .map(rustls::Certificate)
96 1 : .collect_vec()
97 1 : };
98 1 :
99 1 : // needed for channel bindings
100 1 : let first_cert = cert_chain.first().context("missing certificate")?;
101 1 : let tls_server_end_point = TlsServerEndPoint::new(first_cert)?;
102 1 :
103 1 : let tls_config = rustls::ServerConfig::builder()
104 1 : .with_safe_default_cipher_suites()
105 1 : .with_safe_default_kx_groups()
106 1 : .with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12])?
107 1 : .with_no_client_auth()
108 1 : .with_single_cert(cert_chain, key)?
109 1 : .into();
110 1 :
111 1 : (tls_config, tls_server_end_point)
112 1 : }
113 1 : _ => bail!("tls-key and tls-cert must be specified"),
114 1 : };
115 1 :
116 1 : // Start listening for incoming client connections
117 1 : let proxy_address: SocketAddr = args.get_one::<String>("listen").unwrap().parse()?;
118 1 : info!("Starting sni router on {proxy_address}");
119 1 : let proxy_listener = TcpListener::bind(proxy_address).await?;
120 1 :
121 1 : let cancellation_token = CancellationToken::new();
122 1 :
123 1 : let main = tokio::spawn(task_main(
124 1 : Arc::new(destination),
125 1 : tls_config,
126 1 : tls_server_end_point,
127 1 : proxy_listener,
128 1 : cancellation_token.clone(),
129 1 : ));
130 1 : let signals_task = tokio::spawn(proxy::handle_signals(cancellation_token));
131 1 :
132 1 : // the signal task cant ever succeed.
133 1 : // the main task can error, or can succeed on cancellation.
134 1 : // we want to immediately exit on either of these cases
135 1 : let signal = match futures::future::select(signals_task, main).await {
136 1 : Either::Left((res, _)) => proxy::flatten_err(res)?,
137 1 : Either::Right((res, _)) => return proxy::flatten_err(res),
138 1 : };
139 1 :
140 1 : // maintenance tasks return `Infallible` success values, this is an impossible value
141 1 : // so this match statically ensures that there are no possibilities for that value
142 1 : match signal {}
143 1 : }
144 :
145 1 : async fn task_main(
146 1 : dest_suffix: Arc<String>,
147 1 : tls_config: Arc<rustls::ServerConfig>,
148 1 : tls_server_end_point: TlsServerEndPoint,
149 1 : listener: tokio::net::TcpListener,
150 1 : cancellation_token: CancellationToken,
151 1 : ) -> anyhow::Result<()> {
152 1 : // When set for the server socket, the keepalive setting
153 1 : // will be inherited by all accepted client sockets.
154 1 : socket2::SockRef::from(&listener).set_keepalive(true)?;
155 :
156 1 : let connections = tokio_util::task::task_tracker::TaskTracker::new();
157 :
158 2 : while let Some(accept_result) =
159 3 : run_until_cancelled(listener.accept(), &cancellation_token).await
160 : {
161 2 : let (socket, peer_addr) = accept_result?;
162 :
163 2 : let session_id = uuid::Uuid::new_v4();
164 2 : let tls_config = Arc::clone(&tls_config);
165 2 : let dest_suffix = Arc::clone(&dest_suffix);
166 2 :
167 2 : connections.spawn(
168 2 : async move {
169 2 : socket
170 2 : .set_nodelay(true)
171 2 : .context("failed to set socket option")?;
172 :
173 2 : info!(%peer_addr, "serving");
174 2 : let mut ctx =
175 2 : RequestMonitoring::new(session_id, peer_addr.ip(), "sni_router", "sni");
176 2 : handle_client(
177 2 : &mut ctx,
178 2 : dest_suffix,
179 2 : tls_config,
180 2 : tls_server_end_point,
181 2 : socket,
182 2 : )
183 7 : .await
184 2 : }
185 2 : .unwrap_or_else(|e| {
186 2 : // Acknowledge that the task has finished with an error.
187 2 : error!("per-client task finished with an error: {e:#}");
188 2 : })
189 2 : .instrument(tracing::info_span!("handle_client", ?session_id)),
190 : );
191 : }
192 :
193 1 : connections.close();
194 1 : drop(listener);
195 1 :
196 1 : connections.wait().await;
197 :
198 1 : info!("all client connections have finished");
199 1 : Ok(())
200 1 : }
201 :
202 : const ERR_INSECURE_CONNECTION: &str = "connection is insecure (try using `sslmode=require`)";
203 :
204 2 : async fn ssl_handshake<S: AsyncRead + AsyncWrite + Unpin>(
205 2 : raw_stream: S,
206 2 : tls_config: Arc<rustls::ServerConfig>,
207 2 : tls_server_end_point: TlsServerEndPoint,
208 2 : ) -> anyhow::Result<Stream<S>> {
209 2 : let mut stream = PqStream::new(Stream::from_raw(raw_stream));
210 :
211 2 : let msg = stream.read_startup_packet().await?;
212 : use pq_proto::FeStartupPacket::*;
213 :
214 1 : match msg {
215 : SslRequest => {
216 1 : stream
217 1 : .write_message(&pq_proto::BeMessage::EncryptionResponse(true))
218 0 : .await?;
219 : // Upgrade raw stream into a secure TLS-backed stream.
220 : // NOTE: We've consumed `tls`; this fact will be used later.
221 :
222 1 : let (raw, read_buf) = stream.into_inner();
223 1 : // TODO: Normally, client doesn't send any data before
224 1 : // server says TLS handshake is ok and read_buf is empy.
225 1 : // However, you could imagine pipelining of postgres
226 1 : // SSLRequest + TLS ClientHello in one hunk similar to
227 1 : // pipelining in our node js driver. We should probably
228 1 : // support that by chaining read_buf with the stream.
229 1 : if !read_buf.is_empty() {
230 0 : bail!("data is sent before server replied with EncryptionResponse");
231 1 : }
232 1 :
233 1 : Ok(Stream::Tls {
234 2 : tls: Box::new(raw.upgrade(tls_config).await?),
235 1 : tls_server_end_point,
236 : })
237 : }
238 0 : unexpected => {
239 0 : info!(
240 0 : ?unexpected,
241 0 : "unexpected startup packet, rejecting connection"
242 0 : );
243 0 : stream
244 0 : .throw_error_str(ERR_INSECURE_CONNECTION, proxy::error::ErrorKind::User)
245 0 : .await?
246 : }
247 : }
248 2 : }
249 :
250 2 : async fn handle_client(
251 2 : ctx: &mut RequestMonitoring,
252 2 : dest_suffix: Arc<String>,
253 2 : tls_config: Arc<rustls::ServerConfig>,
254 2 : tls_server_end_point: TlsServerEndPoint,
255 2 : stream: impl AsyncRead + AsyncWrite + Unpin,
256 2 : ) -> anyhow::Result<()> {
257 2 : let tls_stream = ssl_handshake(stream, tls_config, tls_server_end_point).await?;
258 :
259 : // Cut off first part of the SNI domain
260 : // We receive required destination details in the format of
261 : // `{k8s_service_name}--{k8s_namespace}--{port}.non-sni-domain`
262 1 : let sni = tls_stream.sni_hostname().ok_or(anyhow!("SNI missing"))?;
263 1 : let dest: Vec<&str> = sni
264 1 : .split_once('.')
265 1 : .context("invalid SNI")?
266 : .0
267 1 : .splitn(3, "--")
268 1 : .collect();
269 1 : let port = dest[2].parse::<u16>().context("invalid port")?;
270 1 : let destination = format!("{}.{}.{}:{}", dest[0], dest[1], dest_suffix, port);
271 :
272 1 : info!("destination: {}", destination);
273 :
274 1 : let client = tokio::net::TcpStream::connect(destination).await?;
275 :
276 1 : let metrics_aux: MetricsAuxInfo = Default::default();
277 1 :
278 1 : // doesn't yet matter as pg-sni-router doesn't report analytics logs
279 1 : ctx.set_success();
280 1 : ctx.log();
281 1 :
282 4 : proxy::proxy::passthrough::proxy_pass(tls_stream, client, metrics_aux).await
283 2 : }
|