Disable TLS session tickets in all clients

A resumed session identifies the client to the server as one it has
talked to before. The app reaches these hosts from many different
tunnels and network locations, so that lets the remote end tie those
connections together over time.

The cost is small. TLS 1.3 full handshakes are already 1-RTT, so what
resumption saved was the certificate chain and a signature check, not a
round trip.
This commit is contained in:
Linus committed 2026-09-08 16:07:57 +02:00
1 parent fe2f6dffa8
commit b43b225318
11 files changed
+32 -10

No files matched your search

+1
View File
@@ -32,6 +32,7 @@ Line wrap the file at 100 chars. Th
- Improve obfuscation performance by using GotaTun. This mainly affects Shadowsocks.
- Do not show dialog when rendering the map fails due to disabled GPU acceleration.
- Require the post-quantum X25519MLKEM768 key exchange for TLS connections to the Mullvad API.
- Disable TLS session tickets to reduce the ability to track clients over time.
#### Linux
- Remove dependency on `iproute2` when using GotaTun with IPv6.
+1
View File
@@ -24,6 +24,7 @@ Line wrap the file at 100 chars. Th
## [Unreleased]
### Changed
- Require the post-quantum X25519MLKEM768 key exchange for TLS connections to the Mullvad API.
- Disable TLS session tickets to reduce the ability to track clients over time.
### Removed
- Remove the initial privacy consent screen. The app now shows the Login Screen on first start.
+4
View File
@@ -143,6 +143,10 @@ All API connections use TLS 1.3 with certificate pinning. The app comes bundled
with servers having a valid certificate issued to `api.mullvad.net` and signed with this
bundled certificate.
TLS session tickets are disabled. Resuming a session identifies the client to the server as one
it has served before, which would let the Mullvad API server link connections made from different
tunnels and different network locations.
#### Tunnel
The networks that "Allow LAN" permits outside the tunnel are blocked outbound *on* the tunnel
+1
View File
@@ -25,6 +25,7 @@ Line wrap the file at 100 chars. Th
### Changed
- Require the post-quantum X25519MLKEM768 key exchange for TLS connections to the Mullvad API.
- Disable TLS session tickets to reduce the ability to track clients over time.
### Added
- Show obfuscation type in connection view.
+7 -6
View File
@@ -309,12 +309,13 @@ async fn cdn_tls_connect(
}
static CDN_TLS_CONFIG: LazyLock<Arc<rustls::ClientConfig>> = LazyLock::new(|| {
Arc::new(
rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(NoCertVerification))
.with_no_client_auth(),
)
let mut config = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(NoCertVerification))
.with_no_client_auth();
// Disable TLS tickets to reduce ability to track clients over time
config.resumption = rustls::client::Resumption::disabled();
Arc::new(config)
});
let connector = tokio_rustls::TlsConnector::from(Arc::clone(&CDN_TLS_CONFIG));
+2
View File
@@ -39,6 +39,8 @@ static TLS_CONFIG: LazyLock<Arc<ClientConfig>> = LazyLock::new(|| {
.with_no_client_auth();
// This assumes that the server hello/certificates will include certificate for the domain.
config.enable_sni = false;
// Disable TLS tickets to reduce ability to track clients over time
config.resumption = rustls::client::Resumption::disabled();
config
};
Arc::new(config)
@@ -129,9 +129,12 @@ fn client_config_tls12() -> ClientConfig {
let root_store = rustls::RootCertStore {
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
};
ClientConfig::builder()
let mut config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth()
.with_no_client_auth();
// Disable TLS tickets to reduce ability to track clients over time
config.resumption = rustls::client::Resumption::disabled();
config
}
#[cfg(test)]
+2
View File
@@ -788,6 +788,8 @@ pub fn default_tls_config() -> Arc<rustls::ClientConfig> {
.with_custom_certificate_verifier(Arc::new(AcceptAnyServerCertificate { provider }))
.with_no_client_auth();
config.alpn_protocols = vec![b"h3".to_vec()];
// Disable TLS tickets to reduce ability to track clients over time
config.resumption = rustls::client::Resumption::disabled();
Arc::new(config)
});
+3 -1
View File
@@ -64,12 +64,14 @@ impl HttpVersionInfoProvider {
roots
.add(defaults::PINNED_CERTIFICATE.clone())
.expect("pinned certificate should be a valid trust anchor");
let tls_config =
let mut tls_config =
ClientConfig::builder_with_provider(Arc::new(aws_lc_rs::default_provider()))
.with_protocol_versions(&[&rustls::version::TLS13])
.expect("aws-lc-rs crypto provider should support TLS 1.3")
.with_root_certificates(roots)
.with_no_client_auth();
// Disable TLS tickets to reduce ability to track clients over time
tls_config.resumption = rustls::client::Resumption::disabled();
let mut req_builder = reqwest::Client::builder().use_preconfigured_tls(tls_config);
// Resolve name without DNS
+4 -1
View File
@@ -42,5 +42,8 @@ pub fn build_client_config(
.expect("aws-lc-rs crypto provider should support default TLS versions")
};
builder.with_root_certificates(roots).with_no_client_auth()
let mut config = builder.with_root_certificates(roots).with_no_client_auth();
// Disable TLS tickets to reduce ability to track clients over time
config.resumption = rustls::client::Resumption::disabled();
config
}
+2
View File
@@ -99,6 +99,8 @@ static CLIENT_CONFIG: LazyLock<ClientConfig> = LazyLock::new(|| {
// The server certificate covers the relevant am.i.mullvad.net hostnames; SNI is omitted
// so the destination subdomain is not visible in the ClientHello.
config.enable_sni = false;
// Disable TLS tickets to reduce ability to track clients over time
config.resumption = rustls::client::Resumption::disabled();
config
});