session resumption

This commit is contained in:
Benjamin Saunders
2018-05-16 00:23:30 -07:00
parent 9e3467c0a2
commit f1e7bd4e54
6 changed files with 105 additions and 31 deletions
+2 -2
View File
@@ -12,7 +12,7 @@ use std::io::{self, Write};
use std::str;
use failure::Error;
use quicr::{Endpoint, Config, Io, Timer, Event, Directionality, ReadError};
use quicr::{Endpoint, Config, Io, Timer, Event, Directionality, ReadError, ClientConfig};
use slog::{Logger, Drain};
fn main() {
@@ -77,7 +77,7 @@ impl Context {
fn run(&mut self) -> Result<()> {
let epoch = Instant::now();
let c = self.client.connect(self.remote, Some(self.remote_host.as_bytes()));
let c = self.client.connect(self.remote, ClientConfig { server_name: Some(&self.remote_host), ..ClientConfig::default() });
let mut time = 0;
let mut buf = Vec::new();
let mut sent = 0;
+36 -8
View File
@@ -14,6 +14,7 @@ use std::net::ToSocketAddrs;
use std::io::{self, Write};
use std::time::{Instant, Duration};
use std::path::PathBuf;
use std::fs;
use futures::Future;
use tokio::runtime::current_thread::Runtime;
@@ -31,10 +32,16 @@ struct Opt {
/// file to log TLS keys to for debugging
#[structopt(parse(from_os_str), long = "keylog")]
keylog: Option<PathBuf>,
url: Url,
/// whether to accept invalid (e.g. self-signed) TLS certificates
#[structopt(long = "accept-insecure-certs")]
accept_insecure_certs: bool,
/// file to read/write session tickets to
#[structopt(long = "session-cache", parse(from_os_str))]
session_cache: Option<PathBuf>,
}
fn main() {
@@ -50,27 +57,48 @@ fn main() {
::std::process::exit(code);
}
fn run(log: Logger, options: Opt) -> Result<()> {
fn run(log: Logger, mut options: Opt) -> Result<()> {
let url = options.url;
let remote = url.with_default_port(|_| Ok(4433))?.to_socket_addrs()?.next().ok_or(format_err!("couldn't resolve to an address"))?;
let mut runtime = Runtime::new()?;
let mut config = quicr::Config {
protocols: vec![b"hq-11"[..].into()],
keylog: options.keylog,
..quicr::Config::default()
};
let ticket;
if let Some(path) = options.session_cache.take() {
ticket = match fs::read(&path) {
Ok(x) => Some(x),
Err(ref e) if e.kind() == io::ErrorKind::NotFound => None,
Err(e) => { return Err(e.into()); }
};
config.session_cache = Some(Box::new(move |_, _, data| {
fs::write(&path, data).unwrap();
}));
} else {
ticket = None;
}
let mut builder = quicr::Endpoint::new();
builder.logger(log.clone())
.config(quicr::Config {
protocols: vec![b"hq-11"[..].into()],
keylog: options.keylog,
accept_insecure_certs: options.accept_insecure_certs,
..quicr::Config::default()
});
.config(config);
let (endpoint, driver, _) = builder.bind("[::]:0")?;
runtime.spawn(driver.map_err(|e| eprintln!("IO error: {}", e)));
let request = format!("GET {}\r\n", url.path());
let start = Instant::now();
runtime.block_on(
endpoint.connect(&remote, url.host_str().map(|x| x.as_bytes()))
endpoint.connect(&remote,
quicr::ClientConfig {
server_name: Some(url.host_str().ok_or(format_err!("URL missing host"))?),
accept_insecure_certs: options.accept_insecure_certs,
session_ticket: ticket.as_ref().map(|x| &x[..]),
..quicr::ClientConfig::default()
})
.map_err(|e| format_err!("failed to connect: {}", e))
.and_then(move |(conn, _)| {
eprintln!("connected at {}", duration_secs(&start.elapsed()));
+3 -5
View File
@@ -89,7 +89,7 @@ use bytes::Bytes;
use quicr::{Directionality, StreamId, ConnectionHandle, Side, CertConfig};
pub use quicr::{Config, ConnectionError, ConnectionId, ListenKeys};
pub use quicr::{Config, ClientConfig, ConnectionError, ConnectionId, ListenKeys};
/// Errors that can occur during the construction of an `Endpoint`.
#[derive(Debug, Fail)]
@@ -307,13 +307,11 @@ impl Endpoint {
}}
/// Connect to a remote endpoint.
///
/// `hostname` is used by the remote endpoint for disambiguation if `addr` hosts multiple services.
pub fn connect(&self, addr: &SocketAddr, hostname: Option<&[u8]>) -> impl Future<Item=(Connection, IncomingStreams), Error=ConnectionError> {
pub fn connect(&self, addr: &SocketAddr, config: ClientConfig) -> impl Future<Item=(Connection, IncomingStreams), Error=ConnectionError> {
let (send, recv) = oneshot::channel();
let conn = {
let mut endpoint = self.0.borrow_mut();
let conn = endpoint.inner.connect(normalize(*addr), hostname);
let conn = endpoint.inner.connect(normalize(*addr), config);
endpoint.pending.insert(conn, Pending::new(Some(send)));
conn
};
+59 -11
View File
@@ -10,9 +10,10 @@ use rand::distributions::Sample;
use slab::Slab;
use openssl::{self, ex_data};
use openssl::ssl::{self, SslContext, SslMethod, SslOptions, SslVersion, SslMode, Ssl, SslStream, HandshakeError, MidHandshakeSslStream,
SslStreamBuilder, SslAlert, SslRef};
SslStreamBuilder, SslAlert, SslRef, SslSession};
use openssl::pkey::{PKeyRef, Private};
use openssl::x509::X509Ref;
use openssl::x509::verify::X509CheckFlags;
use openssl::hash::MessageDigest;
use openssl::symm::{Cipher, encrypt_aead, decrypt_aead};
use blake2::Blake2b;
@@ -92,17 +93,14 @@ pub struct Config {
/// If empty, application-layer protocol negotiation will not be preformed.
pub protocols: Vec<Box<[u8]>>,
/// Whether to accept inauthentic or unverifiable peer certificates.
///
/// Turning this off exposes clients to man-in-the-middle attacks in the same manner as an unencrypted TCP
/// connection, but allows them to connect to servers that are using self-signed certificates.
pub accept_insecure_certs: bool,
/// Path to write NSS SSLKEYLOGFILE-compatible key log.
///
/// Enabling this compromises security by committing secret information to disk. Useful for debugging communications
/// when using tools like Wireshark.
pub keylog: Option<PathBuf>,
/// Function to store session tickets transmitted by the server for fast resumption.
pub session_cache: Option<Box<Fn(Option<&str>, &SocketAddrV6, &[u8]) + Send + Sync + 'static>>,
}
pub struct CertConfig<'a> {
@@ -141,12 +139,37 @@ impl Default for Config {
loss_reduction_factor: 0x8000, // 1/2
protocols: Vec::new(),
accept_insecure_certs: false,
keylog: None,
session_cache: None,
}
}
}
pub struct ClientConfig<'a> {
/// The name of the server the client intends to connect to.
///
/// Used for both certificate validation, and for disambiguating between multiple domains hosted by the same IP
/// address (using SNI).
pub server_name: Option<&'a str>,
/// A ticket to resume a previous session faster than performing a full handshake.
pub session_ticket: Option<&'a [u8]>,
/// Whether to accept inauthentic or unverifiable peer certificates.
///
/// Turning this off exposes clients to man-in-the-middle attacks in the same manner as an unencrypted TCP
/// connection, but allows them to connect to servers that are using self-signed certificates.
pub accept_insecure_certs: bool,
}
impl<'a> Default for ClientConfig<'a> {
fn default() -> Self { Self {
server_name: None,
session_ticket: None,
accept_insecure_certs: false,
}}
}
/// The main entry point to the library
///
/// This object performs no I/O whatsoever. Instead, it generates a stream of I/O operations for a backend to perform
@@ -338,6 +361,16 @@ impl Endpoint {
});
}
if config.session_cache.is_some() {
let config = config.clone();
tls.set_session_cache_mode(ssl::SslSessionCacheMode::CLIENT | ssl::SslSessionCacheMode::NO_INTERNAL_STORE);
tls.set_new_session_callback(move |tls, session| {
let conn = tls.ex_data(*CONNECTION_INFO_INDEX).expect("connection info unset");
let name = tls.servername(ssl::NameType::HOST_NAME);
(config.session_cache.as_ref().unwrap())(name, &conn.remote, &session.to_der().expect("failed to serialize session ticket"));
});
}
let tls = tls.build();
Ok(Self {
@@ -482,15 +515,30 @@ impl Endpoint {
}
/// Initiate a connection
pub fn connect(&mut self, remote: SocketAddrV6, hostname: Option<&[u8]>) -> ConnectionHandle {
pub fn connect(&mut self, remote: SocketAddrV6, config: ClientConfig) -> ConnectionHandle {
let local_id = ConnectionId::random(&mut self.rng, LOCAL_ID_LEN as u8);
let remote_id = ConnectionId::random(&mut self.rng, MAX_CID_SIZE as u8);
trace!(self.log, "initial dcid"; "value" => %remote_id);
let conn = self.add_connection(remote_id.clone(), local_id, remote_id, remote, Side::Client);
let mut tls = Ssl::new(&self.tls).unwrap(); // Is this fallible?
if !self.config.accept_insecure_certs { tls.set_verify(ssl::SslVerifyMode::PEER); }
if !config.accept_insecure_certs {
tls.set_verify(ssl::SslVerifyMode::PEER);
let param = tls.param_mut();
if let Some(name) = config.server_name {
param.set_hostflags(X509CheckFlags::NO_PARTIAL_WILDCARDS);
match name.parse() {
Ok(ip) => { param.set_ip(ip).expect("failed to inform TLS of remote ip"); }
Err(_) => { param.set_host(name).expect("failed to inform TLS of remote hostname"); }
}
}
}
tls.set_ex_data(*CONNECTION_INFO_INDEX, ConnectionInfo { id: self.connections[conn.0].local_id.clone(), remote });
if let Some(hostname) = hostname { tls.set_hostname(str::from_utf8(hostname).expect("malformed hostname")).unwrap(); }
if let Some(name) = config.server_name { tls.set_hostname(name).unwrap(); }
if let Some(session) = config.session_ticket {
if let Err(e) = SslSession::from_der(session).and_then(|x| unsafe { tls.set_session(&x) }) {
error!(self.log, "failed to set TLS session"; "reason" => %e);
}
}
let mut tls = match tls.connect(MemoryStream::new()) {
Ok(_) => unreachable!(),
Err(HandshakeError::WouldBlock(tls)) => tls,
+1 -1
View File
@@ -37,7 +37,7 @@ pub use frame::{ApplicationClose, ConnectionClose};
mod endpoint;
pub use endpoint::{Endpoint, Config, CertConfig, ListenKeys, ConnectionHandle, Event, Io, Timer, ConnectionError, ReadError, WriteError,
ConnectionId, EndpointError};
ConnectionId, EndpointError, ClientConfig};
mod transport_error;
pub use transport_error::Error as TransportError;
+4 -4
View File
@@ -79,7 +79,7 @@ struct Pair {
impl Default for Pair {
fn default() -> Self {
Pair::new(Config { max_remote_uni_streams: 32, max_remote_bi_streams: 32, ..Config::default() },
Config { accept_insecure_certs: true, ..Config::default() })
Config::default())
}
}
@@ -145,7 +145,7 @@ impl Pair {
fn connect(&mut self) -> (ConnectionHandle, ConnectionHandle) {
info!(self.log, "connecting");
let client_conn = self.client.connect(self.server.addr, None);
let client_conn = self.client.connect(self.server.addr, ClientConfig { accept_insecure_certs: true, ..ClientConfig::default() });
self.drive();
let server_conn = if let Some(c) = self.server.accept() { c } else { panic!("server didn't connect"); };
assert_matches!(self.client.poll(), Some((conn, Event::Connected { .. })) if conn == client_conn);
@@ -367,7 +367,7 @@ fn stop_stream() {
fn reject_self_signed_cert() {
let mut pair = Pair::new(Config::default(), Config::default());
info!(pair.log, "connecting");
let client_conn = pair.client.connect(pair.server.addr, None);
let client_conn = pair.client.connect(pair.server.addr, ClientConfig::default());
pair.drive();
assert_matches!(pair.client.poll(),
Some((conn, Event::ConnectionLost { reason: ConnectionError::TransportError {
@@ -398,7 +398,7 @@ fn congestion() {
fn high_latency_handshake() {
let mut pair = Pair::default();
pair.latency = 200 * 1000;
let client_conn = pair.client.connect(pair.server.addr, None);
let client_conn = pair.client.connect(pair.server.addr, ClientConfig { accept_insecure_certs: true, ..ClientConfig::default() });
pair.drive();
let server_conn = if let Some(c) = pair.server.accept() { c } else { panic!("server didn't connect"); };
assert_matches!(pair.client.poll(), Some((conn, Event::Connected { .. })) if conn == client_conn);