1 /*
2 * Copyright (C) 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 //! Quiche Config support
18 //!
19 //! Quiche config objects are needed mutably for constructing a Quiche
20 //! connection object, but not when they are actually being used. As these
21 //! objects include a `SSL_CTX` which can be somewhat expensive and large when
22 //! using a certificate path, it can be beneficial to cache them.
23 //!
24 //! This module provides a caching layer for loading and constructing
25 //! these configurations.
26
27 use quiche::{h3, Result};
28 use std::collections::HashMap;
29 use std::ops::DerefMut;
30 use std::sync::{Arc, RwLock, Weak};
31 use tokio::sync::Mutex;
32
33 type WeakConfig = Weak<Mutex<quiche::Config>>;
34
35 /// A cheaply clonable `quiche::Config`
36 #[derive(Clone)]
37 pub struct Config(Arc<Mutex<quiche::Config>>);
38
39 const MAX_INCOMING_BUFFER_SIZE_WHOLE: u64 = 10000000;
40 const MAX_INCOMING_BUFFER_SIZE_EACH: u64 = 1000000;
41 const MAX_CONCURRENT_STREAM_SIZE: u64 = 100;
42 /// Maximum datagram size we will accept.
43 pub const MAX_DATAGRAM_SIZE: usize = 1350;
44
45 impl Config {
from_weak(weak: &WeakConfig) -> Option<Self>46 fn from_weak(weak: &WeakConfig) -> Option<Self> {
47 weak.upgrade().map(Self)
48 }
49
to_weak(&self) -> WeakConfig50 fn to_weak(&self) -> WeakConfig {
51 Arc::downgrade(&self.0)
52 }
53
54 /// Construct a `Config` object from certificate path. If no path
55 /// is provided, peers will not be verified.
from_key(key: &Key) -> Result<Self>56 pub fn from_key(key: &Key) -> Result<Self> {
57 let mut config = quiche::Config::new(quiche::PROTOCOL_VERSION)?;
58 config.set_application_protos(h3::APPLICATION_PROTOCOL)?;
59 match key.cert_path.as_deref() {
60 Some(path) => {
61 config.verify_peer(true);
62 config.load_verify_locations_from_directory(path)?;
63 }
64 None => config.verify_peer(false),
65 }
66
67 // Some of these configs are necessary, or the server can't respond the HTTP/3 request.
68 config.set_max_idle_timeout(key.max_idle_timeout);
69 config.set_max_recv_udp_payload_size(MAX_DATAGRAM_SIZE);
70 config.set_initial_max_data(MAX_INCOMING_BUFFER_SIZE_WHOLE);
71 config.set_initial_max_stream_data_bidi_local(MAX_INCOMING_BUFFER_SIZE_EACH);
72 config.set_initial_max_stream_data_bidi_remote(MAX_INCOMING_BUFFER_SIZE_EACH);
73 config.set_initial_max_stream_data_uni(MAX_INCOMING_BUFFER_SIZE_EACH);
74 config.set_initial_max_streams_bidi(MAX_CONCURRENT_STREAM_SIZE);
75 config.set_initial_max_streams_uni(MAX_CONCURRENT_STREAM_SIZE);
76 config.set_disable_active_migration(true);
77 Ok(Self(Arc::new(Mutex::new(config))))
78 }
79
80 /// Take the underlying config, usable as `&mut quiche::Config` for use
81 /// with `quiche::connect`.
take(&mut self) -> impl DerefMut<Target = quiche::Config> + '_82 pub async fn take(&mut self) -> impl DerefMut<Target = quiche::Config> + '_ {
83 self.0.lock().await
84 }
85 }
86
87 #[derive(Clone, Default)]
88 struct State {
89 // Mapping from cert_path to configs
90 key_to_config: HashMap<Key, WeakConfig>,
91 // Keep latest config alive to minimize reparsing when flapping
92 // If more keep-alive is needed, replace with a LRU LinkedList
93 latest: Option<Config>,
94 }
95
96 impl State {
get_config(&self, key: &Key) -> Option<Config>97 fn get_config(&self, key: &Key) -> Option<Config> {
98 self.key_to_config.get(key).and_then(Config::from_weak)
99 }
100
keep_alive(&mut self, config: Config)101 fn keep_alive(&mut self, config: Config) {
102 self.latest = Some(config);
103 }
104
garbage_collect(&mut self)105 fn garbage_collect(&mut self) {
106 self.key_to_config.retain(|_, config| config.strong_count() != 0)
107 }
108 }
109
110 /// Cache of Quiche Config objects
111 ///
112 /// Cloning this cache will create another handle to the same cache.
113 ///
114 /// Loading a config object through this caching layer will only keep the
115 /// latest config loaded alive directly, but will still act as a cache
116 /// for any configurations still in use - if the returned `Config` is still
117 /// live, queries to `Cache` will not reconstruct it.
118 #[derive(Clone, Default)]
119 pub struct Cache {
120 // Shared state amongst cache handles
121 state: Arc<RwLock<State>>,
122 }
123
124 /// Key used for getting an associated Quiche Config from Cache.
125 #[derive(Clone, PartialEq, Eq, Hash)]
126 pub struct Key {
127 pub cert_path: Option<String>,
128 pub max_idle_timeout: u64,
129 }
130
131 impl Cache {
132 /// Creates a fresh empty cache
new() -> Self133 pub fn new() -> Self {
134 Default::default()
135 }
136
137 /// Behaves as `Config::from_cert_path`, but with a cache.
138 /// If any object previously given out by this cache is still live,
139 /// a duplicate will not be made.
get(&self, key: &Key) -> Result<Config>140 pub fn get(&self, key: &Key) -> Result<Config> {
141 // Fast path - read-only access to state retrieves config
142 if let Some(config) = self.state.read().unwrap().get_config(key) {
143 return Ok(config);
144 }
145
146 // Unlocked, calculate config. If we have two racing attempts to load
147 // the cert path, we'll arbitrate that in the next step, but this
148 // makes sure loading a new cert path doesn't block other loads to
149 // refresh connections.
150 let config = Config::from_key(key)?;
151
152 let mut state = self.state.write().unwrap();
153 // We now have exclusive access to the state.
154 // If someone else calculated a config at the same time as us, we
155 // want to discard ours and use theirs, since it will result in
156 // less total memory used.
157 if let Some(config) = state.get_config(key) {
158 return Ok(config);
159 }
160
161 // We have exclusive access and a fresh config. Install it into
162 // the cache.
163 state.keep_alive(config.clone());
164 state.key_to_config.insert(key.clone(), config.to_weak());
165 Ok(config)
166 }
167
168 /// Purges any config paths which no longer point to a config entry.
garbage_collect(&self)169 pub fn garbage_collect(&self) {
170 self.state.write().unwrap().garbage_collect();
171 }
172 }
173
174 #[test]
create_quiche_config()175 fn create_quiche_config() {
176 assert!(
177 Config::from_key(&Key { cert_path: None, max_idle_timeout: 1000 }).is_ok(),
178 "quiche config without cert creating failed"
179 );
180 assert!(
181 Config::from_key(&Key {
182 cert_path: Some("data/local/tmp/".to_string()),
183 max_idle_timeout: 1000
184 })
185 .is_ok(),
186 "quiche config with cert creating failed"
187 );
188 }
189
190 #[test]
shared_cache()191 fn shared_cache() {
192 let cache_a = Cache::new();
193 let cache_b = cache_a.clone();
194 let config_a = cache_a.get(&Key { cert_path: None, max_idle_timeout: 1000 }).unwrap();
195 assert_eq!(Arc::strong_count(&config_a.0), 2);
196 let _config_b = cache_b.get(&Key { cert_path: None, max_idle_timeout: 1000 }).unwrap();
197 assert_eq!(Arc::strong_count(&config_a.0), 3);
198 }
199
200 #[test]
different_keys()201 fn different_keys() {
202 let cache = Cache::new();
203 let key_a = Key { cert_path: None, max_idle_timeout: 1000 };
204 let key_b = Key { cert_path: Some("a".to_string()), max_idle_timeout: 1000 };
205 let key_c = Key { cert_path: Some("a".to_string()), max_idle_timeout: 5000 };
206 let config_a = cache.get(&key_a).unwrap();
207 let config_b = cache.get(&key_b).unwrap();
208 let _config_b = cache.get(&key_b).unwrap();
209 let config_c = cache.get(&key_c).unwrap();
210 let _config_c = cache.get(&key_c).unwrap();
211
212 assert_eq!(Arc::strong_count(&config_a.0), 1);
213 assert_eq!(Arc::strong_count(&config_b.0), 2);
214
215 // config_c was most recently created, so it should have an extra strong reference due to
216 // keep-alive in the cache.
217 assert_eq!(Arc::strong_count(&config_c.0), 3);
218 }
219
220 #[test]
lifetimes()221 fn lifetimes() {
222 let cache = Cache::new();
223 let key_a = Key { cert_path: Some("a".to_string()), max_idle_timeout: 1000 };
224 let key_b = Key { cert_path: Some("b".to_string()), max_idle_timeout: 1000 };
225 let config_none = cache.get(&Key { cert_path: None, max_idle_timeout: 1000 }).unwrap();
226 let config_a = cache.get(&key_a).unwrap();
227 let config_b = cache.get(&key_b).unwrap();
228
229 // The first two we created should have a strong count of one - those handles are the only
230 // thing keeping them alive.
231 assert_eq!(Arc::strong_count(&config_none.0), 1);
232 assert_eq!(Arc::strong_count(&config_a.0), 1);
233
234 // If we try to get another handle we already have, it should be the same one.
235 let _config_a2 = cache.get(&key_a).unwrap();
236 assert_eq!(Arc::strong_count(&config_a.0), 2);
237
238 // config_b was most recently created, so it should have a keep-alive
239 // inside the cache.
240 assert_eq!(Arc::strong_count(&config_b.0), 2);
241
242 // If we weaken one of the first handles, then drop it, the weak handle should break
243 let config_none_weak = Config::to_weak(&config_none);
244 assert_eq!(config_none_weak.strong_count(), 1);
245 drop(config_none);
246 assert_eq!(config_none_weak.strong_count(), 0);
247 assert!(Config::from_weak(&config_none_weak).is_none());
248
249 // If we weaken the most *recent* handle, it should keep working
250 let config_b_weak = Config::to_weak(&config_b);
251 assert_eq!(config_b_weak.strong_count(), 2);
252 drop(config_b);
253 assert_eq!(config_b_weak.strong_count(), 1);
254 assert!(Config::from_weak(&config_b_weak).is_some());
255 assert_eq!(config_b_weak.strong_count(), 1);
256
257 // If we try to get a config which is still kept alive by the cache, we should get the same
258 // one.
259 let _config_b2 = cache.get(&key_b).unwrap();
260 assert_eq!(config_b_weak.strong_count(), 2);
261
262 // We broke None, but "a" and "b" should still both be alive. Check that
263 // this is still the case in the mapping after garbage collection.
264 cache.garbage_collect();
265 assert_eq!(cache.state.read().unwrap().key_to_config.len(), 2);
266 }
267
268 #[tokio::test]
quiche_connect()269 async fn quiche_connect() {
270 use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
271 let mut config = Config::from_key(&Key { cert_path: None, max_idle_timeout: 10 }).unwrap();
272 let socket_addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 42));
273 let conn_id = quiche::ConnectionId::from_ref(&[]);
274 quiche::connect(None, &conn_id, socket_addr, config.take().await.deref_mut()).unwrap();
275 }
276