mononoke: allow using ALPN to identify hgcli traffic

Summary:
I'd like to eventually add an actual HTTP stack in Mononoke Server. Doing this
will be easier / nicer if we don't have to peek at the traffic to decide how
to interpret it (i.e. ALPN vs. HTTP).

To do so, we can use ALPN. If the client tells us they want the hgcli protocol,
we can use that, and skip peeking at the traffic entirely.

Obviously, we can't roll this out in one go, so first, let's accept traffic
advertised as hgcli via ALPN. Later, we can stop peeking at the traffic
entirely once we're not receiving any traffic from hgcli not advertised via
ALPN. I added ODS counters so we can canary this.

Note that the way I set this up is that if the client requests something that
isn't ALPN (e.g. H2), we just continue the handshake but don't select anything.

Finally, note that client side, we don't care (or even try to read) what the
server actually selected.

Reviewed By: johansglock

Differential Revision: D25954535

fbshipit-source-id: 183f6f56b2c8895aa8b18101565a4f2cd643be8d
This commit is contained in:
Thomas Orozco 2021-01-20 09:14:11 -08:00 committed by Facebook GitHub Bot
parent 418cf3a5c5
commit d872917609
6 changed files with 52 additions and 20 deletions

View File

@ -7,6 +7,7 @@ license = "GPLv2+"
include = ["src/**/*.rs"] include = ["src/**/*.rs"]
[dependencies] [dependencies]
alpn = { path = "../alpn" }
permission_checker = { path = "../permission_checker" } permission_checker = { path = "../permission_checker" }
scuba_ext = { path = "../common/scuba_ext" } scuba_ext = { path = "../common/scuba_ext" }
session_id = { path = "../server/session_id" } session_id = { path = "../server/session_id" }

View File

@ -325,6 +325,8 @@ impl<'a> StdioRelay<'a> {
connector.set_certificate(&pkcs12.cert)?; connector.set_certificate(&pkcs12.cert)?;
connector.set_private_key(&pkcs12.pkey)?; connector.set_private_key(&pkcs12.pkey)?;
connector.set_alpn_protos(&alpn::alpn_format(alpn::HGCLI_ALPN)?)?;
// add root certificate // add root certificate
connector connector

View File

@ -7,6 +7,7 @@ license = "GPLv2+"
include = ["src/**/*.rs"] include = ["src/**/*.rs"]
[dependencies] [dependencies]
alpn = { path = "../alpn" }
cmdlib = { path = "../cmdlib" } cmdlib = { path = "../cmdlib" }
monitoring = { path = "monitoring" } monitoring = { path = "monitoring" }
repo_listener = { path = "repo_listener" } repo_listener = { path = "repo_listener" }

View File

@ -7,6 +7,7 @@ license = "GPLv2+"
include = ["src/**/*.rs"] include = ["src/**/*.rs"]
[dependencies] [dependencies]
alpn = { path = "../../alpn" }
backsyncer = { path = "../../commit_rewriting/backsyncer" } backsyncer = { path = "../../commit_rewriting/backsyncer" }
blobrepo = { path = "../../blobrepo" } blobrepo = { path = "../../blobrepo" }
blobrepo_factory = { path = "../../blobrepo/factory" } blobrepo_factory = { path = "../../blobrepo/factory" }

View File

@ -50,14 +50,21 @@ use limits::types::MononokeThrottleLimits;
use sshrelay::{ use sshrelay::{
IoStream, Metadata, Preamble, Priority, SshDecoder, SshEncoder, SshEnvVars, SshMsg, Stdio, IoStream, Metadata, Preamble, Priority, SshDecoder, SshEncoder, SshEnvVars, SshMsg, Stdio,
}; };
use stats::prelude::*;
use crate::errors::ErrorKind; use crate::errors::ErrorKind;
use crate::repo_handlers::RepoHandler;
use crate::request_handler::{create_conn_logger, request_handler};
use crate::netspeedtest::{ use crate::netspeedtest::{
create_http_header, handle_http_netspeedtest, parse_netspeedtest_http_params, NetSpeedTest, create_http_header, handle_http_netspeedtest, parse_netspeedtest_http_params, NetSpeedTest,
}; };
use crate::repo_handlers::RepoHandler;
use crate::request_handler::{create_conn_logger, request_handler};
define_stats! {
prefix = "mononoke.connection_acceptor";
http_accepted: timeseries(Sum),
hgcli_accepted: timeseries(Sum),
hgcli_no_alpn_accepted: timeseries(Sum),
}
#[cfg(fbcode_build)] #[cfg(fbcode_build)]
const HEADER_ENCODED_CLIENT_IDENTITY: &str = "x-fb-validated-client-encoded-identity"; const HEADER_ENCODED_CLIENT_IDENTITY: &str = "x-fb-validated-client-encoded-identity";
@ -246,22 +253,35 @@ async fn server_mux(
tls_identities: &MononokeIdentitySet, tls_identities: &MononokeIdentitySet,
logger: &Logger, logger: &Logger,
) -> Result<MuxOutcome> { ) -> Result<MuxOutcome> {
let is_hgcli = s.ssl().selected_alpn_protocol() == Some(alpn::HGCLI_ALPN.as_bytes());
let is_trusted = security_checker.check_if_trusted(&tls_identities).await?; let is_trusted = security_checker.check_if_trusted(&tls_identities).await?;
let (mut rx, mut tx) = tokio::io::split(s); let (mut rx, mut tx) = tokio::io::split(s);
// Elaborate scheme to workaround lack of peek() on AsyncRead // Elaborate scheme to workaround lack of peek() on AsyncRead
let mut peek_buf = vec![0; 4]; let mut peek_buf = vec![0; 4];
rx.read_exact(&mut peek_buf[..]).await?; rx.read_exact(&mut peek_buf[..]).await?;
let is_http = match peek_buf.as_slice() { let is_http = if is_hgcli {
// For non-HTTP connection this can never start with GET or POST as these STATS::hgcli_accepted.add_value(1);
// are wrapped in NetString encoding and prefixed with a type, so false
// should start with: } else {
// <number>:\x00 match peek_buf.as_slice() {
// // For non-HTTP connection this can never start with GET or POST as these
// For example: // are wrapped in NetString encoding and prefixed with a type, so
// 7:\x00hello\n, // should start with:
b"GET " | b"POST" => true, // <number>:\x00
_ => false, //
// For example:
// 7:\x00hello\n,
b"GET " | b"POST" => {
STATS::http_accepted.add_value(1);
true
}
_ => {
STATS::hgcli_no_alpn_accepted.add_value(1);
false
}
}
}; };
let buf_rx = std::io::Cursor::new(peek_buf).chain(BufReader::new(rx)); let buf_rx = std::io::Cursor::new(peek_buf).chain(BufReader::new(rx));

View File

@ -8,16 +8,14 @@
#![deny(warnings)] #![deny(warnings)]
#![feature(never_type)] #![feature(never_type)]
use anyhow::Result; use anyhow::{Context, Result};
use cloned::cloned; use cloned::cloned;
use cmdlib::{args, monitoring::ReadyFlagService}; use cmdlib::{args, monitoring::ReadyFlagService};
use fbinit::FacebookInit; use fbinit::FacebookInit;
use futures::channel::oneshot; use futures::channel::oneshot;
use openssl::ssl::AlpnError;
use slog::{error, info}; use slog::{error, info};
#[cfg(fbcode_build)]
use openssl as _; // suppress unused crate warning - only used outside fbcode
fn setup_app<'a, 'b>() -> args::MononokeClapApp<'a, 'b> { fn setup_app<'a, 'b>() -> args::MononokeClapApp<'a, 'b> {
let app = args::MononokeAppBuilder::new("mononoke server") let app = args::MononokeAppBuilder::new("mononoke server")
.with_shutdown_timeout_args() .with_shutdown_timeout_args()
@ -60,14 +58,23 @@ fn main(fb: FacebookInit) -> Result<()> {
let private_key = matches.value_of("private_key").unwrap().to_string(); let private_key = matches.value_of("private_key").unwrap().to_string();
let ca_pem = matches.value_of("ca_pem").unwrap().to_string(); let ca_pem = matches.value_of("ca_pem").unwrap().to_string();
secure_utils::SslConfig::new( let mut builder = secure_utils::SslConfig::new(
ca_pem, ca_pem,
cert, cert,
private_key, private_key,
matches.value_of("ssl-ticket-seeds"), matches.value_of("ssl-ticket-seeds"),
) )
.build_tls_acceptor(root_log.clone()) .tls_acceptor_builder(root_log.clone())
.expect("failed to build tls acceptor") .context("Failed to instantiate TLS Acceptor builder")?;
builder.set_alpn_select_callback(|_, protos| {
// NOTE: Currently we do not support HTTP/2 here yet.
alpn::alpn_select(protos, alpn::HGCLI_ALPN)
.map_err(|_| AlpnError::ALERT_FATAL)?
.ok_or(AlpnError::NOACK)
});
builder.build()
}; };
info!(root_log, "Creating repo listeners"); info!(root_log, "Creating repo listeners");