mirror of
https://github.com/cfal/shoes.git
synced 2026-11-06 08:14:11 +00:00
Support native hybrid TLS key-exchange allowlists (#177)
Add ordered native rustls key-exchange allowlists for TLS and QUIC. Hybrid-only configurations require TLS 1.3 without classical fallback or early data; omitted settings preserve existing defaults. Reject incompatible or ignored security settings, document configuration, and cover transport and Xray/sing-box interoperability.
This commit is contained in:
@@ -13,6 +13,8 @@ env:
|
||||
SING_BOX_SHA256: 12cb2816b52febb356f6a885b740cc8758c3f30b8ae0ca8edba80f0d2d35343f
|
||||
MIHOMO_VERSION: 1.19.31
|
||||
MIHOMO_SHA256: 04cf9f09671704f839ddbee2e93069dc831a4123a75281e725d1d96ab9ac1afc
|
||||
XRAY_VERSION: 26.3.27
|
||||
XRAY_SHA256: 23cd9af937744d97776ee35ecad4972cf4b2109d1e0fe6be9930467608f7c8ae
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
@@ -139,7 +141,7 @@ jobs:
|
||||
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Install sing-box test peer
|
||||
if: matrix.peer == 'sing-box'
|
||||
if: matrix.peer == 'sing-box' || matrix.target == 'optional_peer'
|
||||
run: |
|
||||
archive="$RUNNER_TEMP/sing-box.tar.gz"
|
||||
curl --fail --location --retry 3 \
|
||||
@@ -164,6 +166,19 @@ jobs:
|
||||
chmod +x "$binary"
|
||||
echo "SHOES_TEST_MIHOMO_BIN=$binary" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install Xray test peer
|
||||
if: matrix.target == 'optional_peer'
|
||||
run: |
|
||||
archive="$RUNNER_TEMP/xray.zip"
|
||||
curl --fail --location --retry 3 \
|
||||
--output "$archive" \
|
||||
"https://github.com/XTLS/Xray-core/releases/download/v${XRAY_VERSION}/Xray-linux-64.zip"
|
||||
echo "${XRAY_SHA256} ${archive}" | sha256sum --check --strict
|
||||
unzip "$archive" xray -d "$RUNNER_TEMP/xray-peer"
|
||||
binary="$RUNNER_TEMP/xray-peer/xray"
|
||||
chmod +x "$binary"
|
||||
echo "SHOES_TEST_XRAY_BIN=$binary" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Run sing-box mux lifecycle tests
|
||||
if: matrix.name == 'local protocols'
|
||||
run: >-
|
||||
|
||||
@@ -82,6 +82,7 @@ quic_settings:
|
||||
client_ca_certs: [string] # Optional client CA certificates
|
||||
client_fingerprints: [string] # Optional client certificate fingerprints
|
||||
num_endpoints: int # Optional, 0 = auto (based on thread count)
|
||||
key_exchange_groups: [X25519MLKEM768] # Optional; see TLS Key Exchange below
|
||||
|
||||
# Routing rules (default: allow-all-direct)
|
||||
rules: string | [RuleConfig]
|
||||
@@ -189,6 +190,7 @@ protocol:
|
||||
client_ca_certs: [string] # Optional client CA certs
|
||||
client_fingerprints: [string] # Optional client cert fingerprints
|
||||
vision: false # Enable Vision (requires VLESS inner protocol)
|
||||
key_exchange_groups: [X25519MLKEM768] # Optional; see TLS Key Exchange below
|
||||
protocol: ServerProxyConfig
|
||||
override_rules: [RuleConfig] # Optional rule override
|
||||
|
||||
@@ -369,6 +371,7 @@ quic_settings:
|
||||
alpn_protocols: [string]
|
||||
cert: string # Client certificate for mTLS
|
||||
key: string # Client key for mTLS
|
||||
key_exchange_groups: [X25519MLKEM768] # Optional; see TLS Key Exchange below
|
||||
```
|
||||
|
||||
## Client Protocols
|
||||
@@ -479,9 +482,56 @@ protocol:
|
||||
cert: string # Client certificate for mTLS
|
||||
key: string # Client key for mTLS
|
||||
vision: false # Enable Vision (requires VLESS inner protocol)
|
||||
key_exchange_groups: [X25519MLKEM768] # Optional; see TLS Key Exchange below
|
||||
protocol: ClientProxyConfig
|
||||
```
|
||||
|
||||
### TLS Key Exchange
|
||||
|
||||
`key_exchange_groups` is an optional, ordered allowlist for ordinary TLS clients,
|
||||
TLS server targets (including `default_tls_target`), and client/server
|
||||
`quic_settings`. It applies to that TLS layer, regardless of the inner proxy
|
||||
protocol. Supported names are case-sensitive:
|
||||
|
||||
- `X25519MLKEM768`: native rustls X25519 + ML-KEM-768 hybrid.
|
||||
- `SecP256r1MLKEM768`: native rustls P-256 + ML-KEM-768 hybrid.
|
||||
- `X25519`, `SecP256r1`, `SecP384r1`: classical groups.
|
||||
|
||||
Omitting the field preserves rustls defaults: currently X25519+ML-KEM-768 is
|
||||
preferred, with classical fallback. An explicit list replaces those defaults;
|
||||
the first client group normally supplies the initial key share. Empty lists, duplicates,
|
||||
null, unknown names, standalone ML-KEM, and `SecP384r1MLKEM1024` are rejected.
|
||||
No custom cryptographic implementations are used.
|
||||
|
||||
Client QUIC allowlists require a non-direct client at the first chain hop
|
||||
(hop 0), where Shoes creates the QUIC transport. Explicit allowlists on direct
|
||||
clients or later hops are rejected, including entries resolved from groups or
|
||||
pools. Use ordinary TLS in a later hop's `protocol` when that hop needs its own
|
||||
hybrid-protected tunnel.
|
||||
|
||||
To require hybrid exchange without classical fallback:
|
||||
|
||||
```yaml
|
||||
key_exchange_groups: [X25519MLKEM768, SecP256r1MLKEM768]
|
||||
```
|
||||
|
||||
Hybrid-only lists force TLS 1.3 and disable early application data. TLS 1.3
|
||||
resumption remains enabled and still performs a fresh exchange using an allowed
|
||||
hybrid group. Peers offering only classical groups or TLS 1.2 cannot connect.
|
||||
For opportunistic hybrid exchange with explicit classical fallback, use a mixed
|
||||
list such as `[X25519MLKEM768, X25519, SecP256r1]`.
|
||||
|
||||
Hybrid-only lists cannot be combined with `vision: true` on the same TLS layer:
|
||||
Vision direct copy bypasses that layer's encryption. They also cannot be
|
||||
combined with TUIC `zero_rtt_handshake: true`. Ordinary outer TLS or QUIC can
|
||||
still protect a separate inner Vision connection. The setting does not apply to
|
||||
REALITY, VLESS encryption, or ShadowTLS camouflage handshakes; it is rejected on
|
||||
ShadowTLS configurations and with deprecated `shadowtls_password`.
|
||||
|
||||
Hybrid key exchange protects session-key establishment, not certificate
|
||||
signatures. Certificate verification, fingerprints, and mTLS work as before.
|
||||
`verify: false` without a trusted fingerprint still permits active impersonation.
|
||||
|
||||
### Reality Client
|
||||
```yaml
|
||||
protocol:
|
||||
|
||||
@@ -194,6 +194,8 @@ where
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct TlsClientConfigTemp {
|
||||
#[serde(default)]
|
||||
key_exchange_groups: super::tls::TlsKeyExchangeGroups,
|
||||
#[serde(default = "default_true")]
|
||||
verify: bool,
|
||||
#[serde(alias = "server_fingerprint", default)]
|
||||
@@ -228,6 +230,11 @@ where
|
||||
|
||||
// Check if deprecated shadowtls_password was used
|
||||
if let Some(password) = temp.shadowtls_password {
|
||||
if !temp.key_exchange_groups.is_default() {
|
||||
return Err(Error::custom(
|
||||
"key_exchange_groups requires ordinary TLS, not deprecated shadowtls_password",
|
||||
));
|
||||
}
|
||||
log::warn!(
|
||||
"The 'shadowtls_password' field in TLS client configuration is deprecated. \
|
||||
Please use 'type: shadowtls' with 'password' field instead. \
|
||||
@@ -236,6 +243,7 @@ where
|
||||
|
||||
// Transform to ShadowTLS variant internally by wrapping protocol
|
||||
return Ok(TlsClientConfig {
|
||||
key_exchange_groups: temp.key_exchange_groups,
|
||||
verify: temp.verify,
|
||||
server_fingerprints: temp.server_fingerprints,
|
||||
sni_hostname: temp.sni_hostname.clone(),
|
||||
@@ -254,6 +262,7 @@ where
|
||||
|
||||
// Normal case - no shadowtls_password
|
||||
Ok(TlsClientConfig {
|
||||
key_exchange_groups: temp.key_exchange_groups,
|
||||
verify: temp.verify,
|
||||
server_fingerprints: temp.server_fingerprints,
|
||||
sni_hostname: temp.sni_hostname,
|
||||
@@ -274,6 +283,24 @@ where
|
||||
deserialize_tls_client_config(deserializer)
|
||||
}
|
||||
|
||||
fn deserialize_shadowtls_variant<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<(String, Option<String>, Box<ClientProxyConfig>), D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ShadowTlsFields {
|
||||
password: String,
|
||||
#[serde(default)]
|
||||
sni_hostname: Option<String>,
|
||||
protocol: Box<ClientProxyConfig>,
|
||||
}
|
||||
let fields = ShadowTlsFields::deserialize(deserializer)?;
|
||||
Ok((fields.password, fields.sni_hostname, fields.protocol))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ClientConfig {
|
||||
@@ -306,6 +333,24 @@ impl Default for ClientConfig {
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientConfig {
|
||||
pub fn validate_quic_key_exchange(&self, initial_hop: bool) -> std::io::Result<()> {
|
||||
let explicit_groups = self
|
||||
.quic_settings
|
||||
.as_ref()
|
||||
.is_some_and(|settings| !settings.key_exchange_groups.is_default());
|
||||
if explicit_groups
|
||||
&& (!initial_hop || self.protocol.is_direct() || self.transport != Transport::Quic)
|
||||
{
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"QUIC key_exchange_groups requires a non-direct client at chain hop 0 with transport: quic",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "lowercase")]
|
||||
pub enum ClientProxyConfig {
|
||||
@@ -384,7 +429,10 @@ pub enum ClientProxyConfig {
|
||||
|
||||
protocol: Box<ClientProxyConfig>,
|
||||
},
|
||||
#[serde(alias = "shadowtls")]
|
||||
#[serde(
|
||||
alias = "shadowtls",
|
||||
deserialize_with = "deserialize_shadowtls_variant"
|
||||
)]
|
||||
ShadowTls {
|
||||
/// ShadowTLS password for authentication
|
||||
password: String,
|
||||
@@ -466,6 +514,11 @@ impl ClientProxyConfig {
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct TlsClientConfig {
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "super::tls::TlsKeyExchangeGroups::is_default"
|
||||
)]
|
||||
pub key_exchange_groups: super::tls::TlsKeyExchangeGroups,
|
||||
#[serde(default = "default_true", skip_serializing_if = "is_true")]
|
||||
pub verify: bool,
|
||||
#[serde(
|
||||
|
||||
@@ -22,6 +22,7 @@ pub mod rules;
|
||||
pub mod selection;
|
||||
pub mod server;
|
||||
pub mod shadowsocks;
|
||||
pub mod tls;
|
||||
pub mod transport;
|
||||
pub mod tun;
|
||||
|
||||
@@ -42,5 +43,6 @@ pub use server::{
|
||||
direct_allow_rule,
|
||||
};
|
||||
pub use shadowsocks::ShadowsocksConfig;
|
||||
pub use tls::TlsKeyExchangeGroups;
|
||||
pub use transport::{BindLocation, ClientQuicConfig, ServerQuicConfig, TcpConfig, Transport};
|
||||
pub use tun::TunConfig;
|
||||
|
||||
@@ -348,6 +348,11 @@ pub struct RealityServerConfig {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct TlsServerConfig {
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "super::tls::TlsKeyExchangeGroups::is_default"
|
||||
)]
|
||||
pub key_exchange_groups: super::tls::TlsKeyExchangeGroups,
|
||||
pub cert: String,
|
||||
pub key: String,
|
||||
#[serde(alias = "alpn_protocol", default)]
|
||||
@@ -389,6 +394,7 @@ pub struct TlsServerConfig {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ShadowTlsServerConfig {
|
||||
pub password: String,
|
||||
pub handshake: ShadowTlsServerHandshakeConfig,
|
||||
@@ -903,6 +909,7 @@ mod tests {
|
||||
transport: Transport::Quic,
|
||||
tcp_settings: None,
|
||||
quic_settings: Some(ServerQuicConfig {
|
||||
key_exchange_groups: Default::default(),
|
||||
cert: "server.crt".to_string(),
|
||||
key: "server.key".to_string(),
|
||||
alpn_protocols: NoneOrSome::Some(vec!["h3".to_string()]),
|
||||
@@ -942,6 +949,7 @@ mod tests {
|
||||
tls_targets.insert(
|
||||
"example.com".to_string(),
|
||||
TlsServerConfig {
|
||||
key_exchange_groups: Default::default(),
|
||||
cert: "example.crt".to_string(),
|
||||
key: "example.key".to_string(),
|
||||
alpn_protocols: NoneOrSome::Some(vec!["h2".to_string(), "http/1.1".to_string()]),
|
||||
@@ -962,6 +970,7 @@ mod tests {
|
||||
protocol: ServerProxyConfig::Tls {
|
||||
tls_targets,
|
||||
default_tls_target: Some(Box::new(TlsServerConfig {
|
||||
key_exchange_groups: Default::default(),
|
||||
cert: "default.crt".to_string(),
|
||||
key: "default.key".to_string(),
|
||||
alpn_protocols: NoneOrSome::None,
|
||||
@@ -1059,6 +1068,7 @@ mod tests {
|
||||
transport: Transport::Quic,
|
||||
tcp_settings: None,
|
||||
quic_settings: Some(ServerQuicConfig {
|
||||
key_exchange_groups: Default::default(),
|
||||
cert: "hysteria.crt".to_string(),
|
||||
key: "hysteria.key".to_string(),
|
||||
alpn_protocols: NoneOrSome::One("hysteria".to_string()),
|
||||
@@ -1083,6 +1093,7 @@ mod tests {
|
||||
transport: Transport::Quic,
|
||||
tcp_settings: None,
|
||||
quic_settings: Some(ServerQuicConfig {
|
||||
key_exchange_groups: Default::default(),
|
||||
cert: "tuic.crt".to_string(),
|
||||
key: "tuic.key".to_string(),
|
||||
alpn_protocols: NoneOrSome::None,
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum TlsKeyExchangeGroup {
|
||||
X25519MLKEM768,
|
||||
SecP256r1MLKEM768,
|
||||
X25519,
|
||||
SecP256r1,
|
||||
SecP384r1,
|
||||
}
|
||||
|
||||
impl TlsKeyExchangeGroup {
|
||||
pub fn is_hybrid(self) -> bool {
|
||||
matches!(self, Self::X25519MLKEM768 | Self::SecP256r1MLKEM768)
|
||||
}
|
||||
|
||||
pub fn rustls_group(self) -> &'static dyn rustls::crypto::SupportedKxGroup {
|
||||
use rustls::crypto::aws_lc_rs::kx_group;
|
||||
match self {
|
||||
Self::X25519MLKEM768 => kx_group::X25519MLKEM768,
|
||||
Self::SecP256r1MLKEM768 => kx_group::SECP256R1MLKEM768,
|
||||
Self::X25519 => kx_group::X25519,
|
||||
Self::SecP256r1 => kx_group::SECP256R1,
|
||||
Self::SecP384r1 => kx_group::SECP384R1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct TlsKeyExchangeGroups(Option<Vec<TlsKeyExchangeGroup>>);
|
||||
|
||||
impl<'de> Deserialize<'de> for TlsKeyExchangeGroups {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let groups = Vec::<TlsKeyExchangeGroup>::deserialize(deserializer)?;
|
||||
if groups.is_empty() {
|
||||
return Err(serde::de::Error::custom(
|
||||
"key_exchange_groups must not be empty",
|
||||
));
|
||||
}
|
||||
for (index, group) in groups.iter().enumerate() {
|
||||
if groups[..index].contains(group) {
|
||||
return Err(serde::de::Error::custom(
|
||||
"duplicate key_exchange_groups entry",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(Self(Some(groups)))
|
||||
}
|
||||
}
|
||||
|
||||
impl TlsKeyExchangeGroups {
|
||||
pub fn is_default(&self) -> bool {
|
||||
self.0.is_none()
|
||||
}
|
||||
|
||||
pub fn groups(&self) -> Option<&[TlsKeyExchangeGroup]> {
|
||||
self.0.as_deref()
|
||||
}
|
||||
|
||||
pub fn requires_hybrid(&self) -> bool {
|
||||
self.groups()
|
||||
.is_some_and(|groups| groups.iter().all(|g| g.is_hybrid()))
|
||||
}
|
||||
|
||||
pub fn validate_vision(&self, vision: bool) -> std::io::Result<()> {
|
||||
if vision && self.requires_hybrid() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"hybrid-only key_exchange_groups cannot be combined with Vision: direct copy bypasses TLS encryption",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_zero_rtt(&self, zero_rtt_handshake: bool) -> std::io::Result<()> {
|
||||
if zero_rtt_handshake && self.requires_hybrid() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"hybrid-only key_exchange_groups cannot be combined with zero_rtt_handshake",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::{ClientProxyConfig, ClientQuicConfig, ServerQuicConfig, TlsServerConfig};
|
||||
|
||||
#[test]
|
||||
fn allowlists_are_nonempty_unique_and_native_only() {
|
||||
for yaml in [
|
||||
"[]",
|
||||
"null",
|
||||
"[X25519, X25519]",
|
||||
"[SecP384r1MLKEM1024]",
|
||||
"[MLKEM768]",
|
||||
"[typo]",
|
||||
] {
|
||||
assert!(
|
||||
serde_yaml::from_str::<TlsKeyExchangeGroups>(yaml).is_err(),
|
||||
"{yaml}"
|
||||
);
|
||||
}
|
||||
for (yaml, strict) in [
|
||||
("[X25519MLKEM768]", true),
|
||||
("[SecP256r1MLKEM768, X25519MLKEM768]", true),
|
||||
("[X25519MLKEM768, X25519]", false),
|
||||
("[SecP256r1, SecP384r1]", false),
|
||||
] {
|
||||
let groups: TlsKeyExchangeGroups = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(groups.requires_hybrid(), strict);
|
||||
assert_eq!(
|
||||
groups,
|
||||
serde_yaml::from_str(&serde_yaml::to_string(&groups).unwrap()).unwrap()
|
||||
);
|
||||
assert_eq!(groups.validate_vision(true).is_err(), strict);
|
||||
assert_eq!(groups.validate_zero_rtt(true).is_err(), strict);
|
||||
assert!(groups.validate_vision(false).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_tls_and_quic_share_the_allowlist() {
|
||||
for field in ["", "key_exchange_groups: [SecP256r1MLKEM768]\n"] {
|
||||
let strict = !field.is_empty();
|
||||
let client: ClientProxyConfig =
|
||||
serde_yaml::from_str(&format!("type: tls\n{field}protocol: {{type: socks}}\n"))
|
||||
.unwrap();
|
||||
let ClientProxyConfig::Tls(tls) = &client else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(tls.key_exchange_groups.requires_hybrid(), strict);
|
||||
let serialized = serde_yaml::to_string(&client).unwrap();
|
||||
assert_eq!(serialized.contains("key_exchange_groups"), strict);
|
||||
assert!(serde_yaml::from_str::<ClientProxyConfig>(&serialized).is_ok());
|
||||
let server: TlsServerConfig = serde_yaml::from_str(&format!(
|
||||
"{field}cert: cert\nkey: key\nprotocol: {{type: socks}}\n"
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(server.key_exchange_groups.requires_hybrid(), strict);
|
||||
let client: ClientQuicConfig =
|
||||
serde_yaml::from_str(&format!("verify: true\n{field}")).unwrap();
|
||||
assert_eq!(client.key_exchange_groups.requires_hybrid(), strict);
|
||||
let server: ServerQuicConfig =
|
||||
serde_yaml::from_str(&format!("cert: cert\nkey: key\n{field}")).unwrap();
|
||||
assert_eq!(server.key_exchange_groups.requires_hybrid(), strict);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shadowtls_cannot_silently_ignore_allowlists() {
|
||||
for yaml in [
|
||||
"type: shadowtls\npassword: password\nkey_exchange_groups: [X25519MLKEM768]\nprotocol: {type: socks}",
|
||||
"type: tls\nshadowtls_password: password\nkey_exchange_groups: [X25519MLKEM768]\nprotocol: {type: socks}",
|
||||
] {
|
||||
assert!(serde_yaml::from_str::<ClientProxyConfig>(yaml).is_err());
|
||||
}
|
||||
let yaml = "password: password\nkey_exchange_groups: [X25519MLKEM768]\nhandshake: {cert: cert, key: key}\nprotocol: {type: socks}";
|
||||
assert!(serde_yaml::from_str::<crate::config::ShadowTlsServerConfig>(yaml).is_err());
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,11 @@ impl Default for TcpConfig {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ServerQuicConfig {
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "super::tls::TlsKeyExchangeGroups::is_default"
|
||||
)]
|
||||
pub key_exchange_groups: super::tls::TlsKeyExchangeGroups,
|
||||
pub cert: String,
|
||||
pub key: String,
|
||||
#[serde(alias = "alpn_protocol", default)]
|
||||
@@ -79,6 +84,11 @@ pub struct ServerQuicConfig {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ClientQuicConfig {
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "super::tls::TlsKeyExchangeGroups::is_default"
|
||||
)]
|
||||
pub key_exchange_groups: super::tls::TlsKeyExchangeGroups,
|
||||
#[serde(default = "default_true")]
|
||||
pub verify: bool,
|
||||
#[serde(alias = "server_fingerprint", default)]
|
||||
@@ -96,6 +106,7 @@ pub struct ClientQuicConfig {
|
||||
impl Default for ClientQuicConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
key_exchange_groups: Default::default(),
|
||||
verify: true,
|
||||
server_fingerprints: NoneOrSome::Unspecified,
|
||||
sni_hostname: NoneOrOne::Unspecified,
|
||||
|
||||
@@ -650,6 +650,14 @@ fn validate_server_config(
|
||||
}
|
||||
// First handle QUIC settings certificates
|
||||
if let Some(ref mut quic_settings) = server_config.quic_settings {
|
||||
if let ServerProxyConfig::TuicV5 {
|
||||
zero_rtt_handshake, ..
|
||||
} = &server_config.protocol
|
||||
{
|
||||
quic_settings
|
||||
.key_exchange_groups
|
||||
.validate_zero_rtt(*zero_rtt_handshake)?;
|
||||
}
|
||||
embed_pem_from_map(&mut quic_settings.cert, named_pems);
|
||||
embed_pem_from_map(&mut quic_settings.key, named_pems);
|
||||
for cert in quic_settings.client_ca_certs.iter_mut() {
|
||||
@@ -875,6 +883,9 @@ fn validate_client_vision_protocol(
|
||||
fn validate_client_proxy_structure(config: &ClientProxyConfig) -> std::io::Result<()> {
|
||||
let inner = match config {
|
||||
ClientProxyConfig::Tls(tls_config) => {
|
||||
tls_config
|
||||
.key_exchange_groups
|
||||
.validate_vision(tls_config.vision)?;
|
||||
validate_client_vision_protocol(tls_config.vision, &tls_config.protocol, "TLS")?;
|
||||
Some(tls_config.protocol.as_ref())
|
||||
}
|
||||
@@ -904,6 +915,7 @@ fn validate_client_config(
|
||||
client_config: &mut ClientConfig,
|
||||
named_pems: &HashMap<String, String>,
|
||||
) -> std::io::Result<()> {
|
||||
client_config.validate_quic_key_exchange(true)?;
|
||||
if client_config.transport != Transport::Tcp && client_config.tcp_settings.is_some() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
@@ -1096,6 +1108,7 @@ fn validate_server_certificates(
|
||||
ca.iter().map(|pem| pem.as_bytes().to_vec()).collect(),
|
||||
&[],
|
||||
&[],
|
||||
&Default::default(),
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
@@ -1109,6 +1122,7 @@ fn validate_client_certificates(cert: Option<&str>, key: Option<&str>) -> std::i
|
||||
true,
|
||||
Some((key.as_bytes().to_vec(), cert.as_bytes().to_vec())),
|
||||
false,
|
||||
&Default::default(),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -1264,6 +1278,9 @@ fn validate_server_proxy_config(
|
||||
));
|
||||
}
|
||||
for tls_server_config in tls_targets.values_mut() {
|
||||
tls_server_config
|
||||
.key_exchange_groups
|
||||
.validate_vision(tls_server_config.vision)?;
|
||||
validate_server_vision(tls_server_config.vision, &tls_server_config.protocol)?;
|
||||
embed_pem_from_map(&mut tls_server_config.cert, named_pems);
|
||||
embed_pem_from_map(&mut tls_server_config.key, named_pems);
|
||||
@@ -1304,6 +1321,9 @@ fn validate_server_proxy_config(
|
||||
}
|
||||
}
|
||||
if let Some(tls_server_config) = default_tls_target {
|
||||
tls_server_config
|
||||
.key_exchange_groups
|
||||
.validate_vision(tls_server_config.vision)?;
|
||||
validate_server_vision(tls_server_config.vision, &tls_server_config.protocol)?;
|
||||
embed_pem_from_map(&mut tls_server_config.cert, named_pems);
|
||||
embed_pem_from_map(&mut tls_server_config.key, named_pems);
|
||||
@@ -1725,6 +1745,21 @@ fn expand_client_chain(
|
||||
.map(|hop| expand_chain_hop(hop, client_groups))
|
||||
.collect::<std::io::Result<Vec<_>>>()?;
|
||||
|
||||
for (index, hop) in expanded_hops.iter().enumerate() {
|
||||
let validate = |selection: &ConfigSelection<ClientConfig>| match selection {
|
||||
ConfigSelection::Config(config) => config.validate_quic_key_exchange(index == 0),
|
||||
ConfigSelection::GroupName(_) => unreachable!("groups have been expanded"),
|
||||
};
|
||||
match hop {
|
||||
ClientChainHop::Single(selection) => validate(selection)?,
|
||||
ClientChainHop::Pool(selections) => {
|
||||
for selection in selections.iter() {
|
||||
validate(selection)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*client_chain = if expanded_hops.len() == 1 {
|
||||
OneOrSome::One(expanded_hops.into_iter().next().unwrap())
|
||||
} else {
|
||||
@@ -2028,6 +2063,111 @@ mod tests {
|
||||
Ok(validated.configs)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hybrid_only_rejects_vision_and_tuic_early_handshakes() {
|
||||
let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap();
|
||||
let tls_target = serde_json::json!({
|
||||
"cert": cert.cert.pem(), "key": cert.signing_key.serialize_pem(),
|
||||
"key_exchange_groups": ["X25519MLKEM768"], "vision": true,
|
||||
"protocol": {"type": "vless", "user_id": "550e8400-e29b-41d4-a716-446655440000"}
|
||||
});
|
||||
for protocol in [
|
||||
serde_json::json!({"type": "tls", "tls_targets": {"localhost": tls_target}}),
|
||||
serde_json::json!({"type": "tls", "default_tls_target": tls_target}),
|
||||
] {
|
||||
let mut server: ServerProxyConfig = serde_json::from_value(protocol).unwrap();
|
||||
let error = validate_server_proxy_config(
|
||||
&mut server,
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
ServerProtocolContext::Stream,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(error.to_string().contains("hybrid-only"), "{error}");
|
||||
}
|
||||
let client: ClientProxyConfig = serde_json::from_value(serde_json::json!({
|
||||
"type": "tls", "key_exchange_groups": ["SecP256r1MLKEM768"], "vision": true,
|
||||
"protocol": {"type": "vless", "user_id": "550e8400-e29b-41d4-a716-446655440000"}
|
||||
}))
|
||||
.unwrap();
|
||||
assert!(
|
||||
validate_client_proxy_structure(&client)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("hybrid-only")
|
||||
);
|
||||
let mut server: ServerConfig = serde_json::from_value(serde_json::json!({
|
||||
"address": "0.0.0.0:443", "transport": "quic",
|
||||
"quic_settings": {"cert": cert.cert.pem(), "key": cert.signing_key.serialize_pem(), "key_exchange_groups": ["X25519MLKEM768"]},
|
||||
"protocol": {"type": "tuic", "uuid": "550e8400-e29b-41d4-a716-446655440000", "password": "test", "zero_rtt_handshake": true}
|
||||
})).unwrap();
|
||||
assert!(
|
||||
validate_server_config(
|
||||
&mut server,
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
&HashMap::new()
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("zero_rtt_handshake")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quic_allowlists_cannot_be_ignored_by_direct_or_later_hops() {
|
||||
use serde_json::json;
|
||||
for groups in [
|
||||
None,
|
||||
Some(json!(["X25519MLKEM768"])),
|
||||
Some(json!(["X25519"])),
|
||||
] {
|
||||
let mut settings = json!({});
|
||||
if let Some(ref groups) = groups {
|
||||
settings["key_exchange_groups"] = groups.clone();
|
||||
}
|
||||
let direct = json!({"protocol": {"type": "direct"}, "transport": "quic", "quic_settings": settings});
|
||||
let mut config: ClientConfig = serde_json::from_value(direct).unwrap();
|
||||
let result = validate_client_config(&mut config, &HashMap::new());
|
||||
assert_eq!(result.is_err(), groups.is_some());
|
||||
|
||||
let proxy = json!({"address": "127.0.0.1:443", "protocol": {"type": "socks"}, "transport": "quic", "quic_settings": settings});
|
||||
let group_configs = HashMap::from([(
|
||||
"proxy".into(),
|
||||
vec![serde_json::from_value::<ClientConfig>(proxy.clone()).unwrap()],
|
||||
)]);
|
||||
for hop in [
|
||||
proxy.clone(),
|
||||
json!("proxy"),
|
||||
json!({"pool": ["proxy", proxy.clone()]}),
|
||||
] {
|
||||
let direct = json!({"protocol": {"type": "direct"}});
|
||||
for chain in [json!([hop]), json!([direct, hop])] {
|
||||
let later_hop = chain.as_array().unwrap().len() > 1;
|
||||
let mut parsed: super::super::types::ClientChain =
|
||||
serde_json::from_value(chain.clone()).unwrap();
|
||||
let result =
|
||||
validate_client_chain(&mut parsed.hops, 0, &group_configs, &HashMap::new());
|
||||
assert_eq!(result.is_err(), later_hop && groups.is_some(), "{chain}");
|
||||
if let Err(error) = result {
|
||||
assert!(error.to_string().contains("QUIC key_exchange_groups"));
|
||||
}
|
||||
let spec: DnsServerSpec = serde_json::from_value(json!({
|
||||
"url": "https://1.1.1.1/dns-query", "client_chain": {"chain": chain}
|
||||
}))
|
||||
.unwrap();
|
||||
let result =
|
||||
expand_dns_specs(&[spec], &group_configs, &HashMap::new(), &HashSet::new());
|
||||
assert_eq!(result.is_err(), later_hop && groups.is_some());
|
||||
if let Err(error) = result {
|
||||
assert!(error.to_string().contains("QUIC key_exchange_groups"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_validate_config_success() {
|
||||
use crate::config::types::ClientConfigGroup;
|
||||
|
||||
+12
-1
@@ -157,6 +157,7 @@ pub async fn start_quic_servers(
|
||||
};
|
||||
|
||||
let ServerQuicConfig {
|
||||
key_exchange_groups,
|
||||
cert,
|
||||
key,
|
||||
client_ca_certs,
|
||||
@@ -164,6 +165,12 @@ pub async fn start_quic_servers(
|
||||
client_fingerprints,
|
||||
num_endpoints,
|
||||
} = quic_settings.unwrap();
|
||||
if let ServerProxyConfig::TuicV5 {
|
||||
zero_rtt_handshake, ..
|
||||
} = &protocol
|
||||
{
|
||||
key_exchange_groups.validate_zero_rtt(*zero_rtt_handshake)?;
|
||||
}
|
||||
|
||||
// Certificates are already embedded as PEM data during config validation
|
||||
let cert_bytes = cert.as_bytes().to_vec();
|
||||
@@ -180,9 +187,12 @@ pub async fn start_quic_servers(
|
||||
processed_ca_certs,
|
||||
&alpn_protocols.into_vec(),
|
||||
&client_fingerprints.into_vec(),
|
||||
&key_exchange_groups,
|
||||
)?;
|
||||
// QUIC manages early data separately; its TLS limit must be zero or u32::MAX.
|
||||
server_config.max_early_data_size = u32::MAX;
|
||||
if !key_exchange_groups.requires_hybrid() {
|
||||
server_config.max_early_data_size = u32::MAX;
|
||||
}
|
||||
let server_config = Arc::new(server_config);
|
||||
|
||||
let quic_server_config: quinn::crypto::rustls::QuicServerConfig = server_config
|
||||
@@ -335,6 +345,7 @@ mod tests {
|
||||
transport: Transport::Quic,
|
||||
tcp_settings: None,
|
||||
quic_settings: Some(ServerQuicConfig {
|
||||
key_exchange_groups: Default::default(),
|
||||
cert: cert.cert.pem(),
|
||||
key: cert.signing_key.serialize_pem(),
|
||||
alpn_protocols: NoneOrSome::One("h3".into()),
|
||||
|
||||
+56
-13
@@ -4,6 +4,11 @@ use std::sync::OnceLock;
|
||||
|
||||
use rustls::pki_types::pem::PemObject;
|
||||
|
||||
use crate::config::TlsKeyExchangeGroups;
|
||||
|
||||
#[cfg(test)]
|
||||
mod hybrid_tests;
|
||||
|
||||
pub fn try_create_client_config(
|
||||
verify_webpki: bool,
|
||||
server_fingerprints: Vec<String>,
|
||||
@@ -11,15 +16,17 @@ pub fn try_create_client_config(
|
||||
enable_sni: bool,
|
||||
client_key_and_cert: Option<(Vec<u8>, Vec<u8>)>,
|
||||
tls13_only: bool,
|
||||
key_exchange_groups: &TlsKeyExchangeGroups,
|
||||
) -> std::io::Result<rustls::ClientConfig> {
|
||||
let builder = rustls::ClientConfig::builder_with_provider(get_crypto_provider());
|
||||
let builder = if tls13_only {
|
||||
builder
|
||||
.with_protocol_versions(&[&rustls::version::TLS13])
|
||||
.unwrap()
|
||||
let builder = rustls::ClientConfig::builder_with_provider(crypto_provider_with_groups(
|
||||
key_exchange_groups,
|
||||
));
|
||||
let builder = if tls13_only || key_exchange_groups.requires_hybrid() {
|
||||
builder.with_protocol_versions(&[&rustls::version::TLS13])
|
||||
} else {
|
||||
builder.with_safe_default_protocol_versions().unwrap()
|
||||
};
|
||||
builder.with_safe_default_protocol_versions()
|
||||
}
|
||||
.map_err(std::io::Error::other)?;
|
||||
|
||||
let builder = if verify_webpki {
|
||||
let webpki_verifier = rustls::client::WebPkiServerVerifier::builder_with_provider(
|
||||
@@ -75,6 +82,9 @@ pub fn try_create_client_config(
|
||||
.collect();
|
||||
|
||||
config.enable_sni = enable_sni;
|
||||
if key_exchange_groups.requires_hybrid() {
|
||||
config.enable_early_data = false;
|
||||
}
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
@@ -197,6 +207,17 @@ fn get_supported_algorithms() -> rustls::crypto::WebPkiSupportedAlgorithms {
|
||||
get_crypto_provider().signature_verification_algorithms
|
||||
}
|
||||
|
||||
fn crypto_provider_with_groups(
|
||||
groups: &TlsKeyExchangeGroups,
|
||||
) -> Arc<rustls::crypto::CryptoProvider> {
|
||||
let Some(groups) = groups.groups() else {
|
||||
return get_crypto_provider();
|
||||
};
|
||||
let mut provider = (*get_crypto_provider()).clone();
|
||||
provider.kx_groups = groups.iter().map(|group| group.rustls_group()).collect();
|
||||
Arc::new(provider)
|
||||
}
|
||||
|
||||
fn get_disabled_verifier() -> Arc<DisabledVerifier> {
|
||||
static INSTANCE: OnceLock<Arc<DisabledVerifier>> = OnceLock::new();
|
||||
INSTANCE
|
||||
@@ -236,6 +257,7 @@ pub fn try_create_server_config(
|
||||
ca_cert_bytes: Vec<Vec<u8>>,
|
||||
alpn_protocols: &[String],
|
||||
client_fingerprints: &[String],
|
||||
key_exchange_groups: &TlsKeyExchangeGroups,
|
||||
) -> std::io::Result<rustls::ServerConfig> {
|
||||
// Parse all certificates from the PEM file (server cert + intermediates)
|
||||
let certs: Vec<_> = rustls::pki_types::CertificateDer::pem_slice_iter(cert_bytes)
|
||||
@@ -269,9 +291,15 @@ pub fn try_create_server_config(
|
||||
Some(verifier)
|
||||
};
|
||||
|
||||
let builder = rustls::ServerConfig::builder_with_provider(get_crypto_provider())
|
||||
.with_safe_default_protocol_versions()
|
||||
.unwrap();
|
||||
let builder = rustls::ServerConfig::builder_with_provider(crypto_provider_with_groups(
|
||||
key_exchange_groups,
|
||||
));
|
||||
let builder = if key_exchange_groups.requires_hybrid() {
|
||||
builder.with_protocol_versions(&[&rustls::version::TLS13])
|
||||
} else {
|
||||
builder.with_safe_default_protocol_versions()
|
||||
}
|
||||
.map_err(std::io::Error::other)?;
|
||||
// Always wraps in ClientFingerprintVerifier even for CA-only auth, because
|
||||
// WebPkiClientVerifier's root_hint_subjects() leaks CA names to unauthenticated clients.
|
||||
let builder = if client_fingerprints.is_empty() && webpki_verifier.is_none() {
|
||||
@@ -293,6 +321,10 @@ pub fn try_create_server_config(
|
||||
.collect();
|
||||
|
||||
config.max_fragment_size = None;
|
||||
if key_exchange_groups.requires_hybrid() {
|
||||
config.max_early_data_size = 0;
|
||||
config.send_half_rtt_data = false;
|
||||
}
|
||||
config.ignore_client_order = true;
|
||||
|
||||
Ok(config)
|
||||
@@ -344,6 +376,7 @@ mod material_tests {
|
||||
vec![],
|
||||
&[],
|
||||
&[],
|
||||
&Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut roots = rustls::RootCertStore::empty();
|
||||
@@ -493,7 +526,15 @@ mod material_tests {
|
||||
(pem.as_str(), wrong_key.as_str(), false),
|
||||
] {
|
||||
assert_eq!(
|
||||
try_create_server_config(cert.as_bytes(), key.as_bytes(), vec![], &[], &[]).is_ok(),
|
||||
try_create_server_config(
|
||||
cert.as_bytes(),
|
||||
key.as_bytes(),
|
||||
vec![],
|
||||
&[],
|
||||
&[],
|
||||
&Default::default()
|
||||
)
|
||||
.is_ok(),
|
||||
valid
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -503,7 +544,8 @@ mod material_tests {
|
||||
vec![],
|
||||
true,
|
||||
Some((key.as_bytes().to_vec(), cert.as_bytes().to_vec())),
|
||||
false
|
||||
false,
|
||||
&Default::default(),
|
||||
)
|
||||
.is_ok(),
|
||||
valid
|
||||
@@ -515,7 +557,8 @@ mod material_tests {
|
||||
key.as_bytes(),
|
||||
vec![b"invalid".to_vec()],
|
||||
&[],
|
||||
&[]
|
||||
&[],
|
||||
&Default::default(),
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
use super::*;
|
||||
use rustls::{ClientConnection, HandshakeKind, NamedGroup, ServerConnection};
|
||||
use std::io::{Read, Write};
|
||||
|
||||
fn configured_groups(names: &str) -> TlsKeyExchangeGroups {
|
||||
if names.is_empty() {
|
||||
return TlsKeyExchangeGroups::default();
|
||||
}
|
||||
serde_yaml::from_str(&format!("[{names}]")).unwrap()
|
||||
}
|
||||
|
||||
fn fingerprint(cert: &rcgen::Certificate) -> String {
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, cert.der())
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|b| format!("{b:02x}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn configs(
|
||||
client_groups: &str,
|
||||
server_groups: &str,
|
||||
) -> (rustls::ClientConfig, rustls::ServerConfig) {
|
||||
let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap();
|
||||
let client = try_create_client_config(
|
||||
false,
|
||||
vec![fingerprint(&cert.cert)],
|
||||
vec!["hybrid-test".into()],
|
||||
true,
|
||||
None,
|
||||
false,
|
||||
&configured_groups(client_groups),
|
||||
)
|
||||
.unwrap();
|
||||
let server = try_create_server_config(
|
||||
cert.cert.pem().as_bytes(),
|
||||
cert.signing_key.serialize_pem().as_bytes(),
|
||||
vec![],
|
||||
&["hybrid-test".into()],
|
||||
&[],
|
||||
&configured_groups(server_groups),
|
||||
)
|
||||
.unwrap();
|
||||
(client, server)
|
||||
}
|
||||
|
||||
fn handshake(
|
||||
client: Arc<rustls::ClientConfig>,
|
||||
server: Arc<rustls::ServerConfig>,
|
||||
) -> Result<(ClientConnection, ServerConnection), rustls::Error> {
|
||||
let mut client = ClientConnection::new(client, "localhost".try_into().unwrap()).unwrap();
|
||||
let mut server = ServerConnection::new(server).unwrap();
|
||||
for _ in 0..16 {
|
||||
let mut wire = Vec::new();
|
||||
client.write_tls(&mut wire).unwrap();
|
||||
for fragment in wire.chunks(37) {
|
||||
server.read_tls(&mut &fragment[..]).unwrap();
|
||||
server.process_new_packets()?;
|
||||
}
|
||||
wire.clear();
|
||||
server.write_tls(&mut wire).unwrap();
|
||||
for fragment in wire.chunks(37) {
|
||||
client.read_tls(&mut &fragment[..]).unwrap();
|
||||
client.process_new_packets()?;
|
||||
}
|
||||
if !client.is_handshaking()
|
||||
&& !server.is_handshaking()
|
||||
&& !client.wants_write()
|
||||
&& !server.wants_write()
|
||||
{
|
||||
return Ok((client, server));
|
||||
}
|
||||
}
|
||||
panic!("handshake did not converge");
|
||||
}
|
||||
|
||||
fn assert_group(client: &ClientConnection, server: &ServerConnection, group: NamedGroup) {
|
||||
assert_eq!(
|
||||
client.protocol_version(),
|
||||
Some(rustls::ProtocolVersion::TLSv1_3)
|
||||
);
|
||||
assert_eq!(
|
||||
server.protocol_version(),
|
||||
Some(rustls::ProtocolVersion::TLSv1_3)
|
||||
);
|
||||
assert_eq!(
|
||||
client.negotiated_key_exchange_group().unwrap().name(),
|
||||
group
|
||||
);
|
||||
assert_eq!(
|
||||
server.negotiated_key_exchange_group().unwrap().name(),
|
||||
group
|
||||
);
|
||||
assert_eq!(client.alpn_protocol(), Some(b"hybrid-test".as_slice()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_hybrids_exchange_data_and_resume_with_fresh_hybrid_keys() {
|
||||
for (name, group) in [
|
||||
("X25519MLKEM768", NamedGroup::X25519MLKEM768),
|
||||
("SecP256r1MLKEM768", NamedGroup::secp256r1MLKEM768),
|
||||
] {
|
||||
let (client_config, server_config) = configs(name, name);
|
||||
assert!(!client_config.enable_early_data);
|
||||
assert_eq!(server_config.max_early_data_size, 0);
|
||||
assert!(!server_config.send_half_rtt_data);
|
||||
let client_config = Arc::new(client_config);
|
||||
let server_config = Arc::new(server_config);
|
||||
for kind in [HandshakeKind::Full, HandshakeKind::Resumed] {
|
||||
let (mut client, mut server) =
|
||||
handshake(client_config.clone(), server_config.clone()).unwrap();
|
||||
assert_group(&client, &server, group);
|
||||
assert_eq!(client.handshake_kind(), Some(kind));
|
||||
assert_eq!(server.handshake_kind(), Some(kind));
|
||||
assert!(client.early_data().is_none());
|
||||
assert!(server.early_data().is_none());
|
||||
client.writer().write_all(b"client payload").unwrap();
|
||||
let mut wire = Vec::new();
|
||||
client.write_tls(&mut wire).unwrap();
|
||||
server.read_tls(&mut wire.as_slice()).unwrap();
|
||||
server.process_new_packets().unwrap();
|
||||
let mut payload = [0; 14];
|
||||
server.reader().read_exact(&mut payload).unwrap();
|
||||
assert_eq!(&payload, b"client payload");
|
||||
server.writer().write_all(b"server payload").unwrap();
|
||||
wire.clear();
|
||||
server.write_tls(&mut wire).unwrap();
|
||||
client.read_tls(&mut wire.as_slice()).unwrap();
|
||||
client.process_new_packets().unwrap();
|
||||
client.reader().read_exact(&mut payload).unwrap();
|
||||
assert_eq!(&payload, b"server payload");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hybrid_only_rejects_classical_peers_in_both_roles() {
|
||||
for hybrid in ["X25519MLKEM768", "SecP256r1MLKEM768"] {
|
||||
for classical in ["X25519", "SecP256r1", "SecP384r1"] {
|
||||
for (client, server) in [(hybrid, classical), (classical, hybrid)] {
|
||||
let (client, server) = configs(client, server);
|
||||
assert!(handshake(Arc::new(client), Arc::new(server)).is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
let (client, server) = configs("X25519MLKEM768", "SecP256r1MLKEM768");
|
||||
assert!(handshake(Arc::new(client), Arc::new(server)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hybrid_only_rejects_tls12_in_both_roles() {
|
||||
for hybrid in ["X25519MLKEM768", "SecP256r1MLKEM768"] {
|
||||
let (client, mut server) = configs(hybrid, "X25519");
|
||||
server = rustls::ServerConfig::builder_with_provider(server.crypto_provider().clone())
|
||||
.with_protocol_versions(&[&rustls::version::TLS12])
|
||||
.unwrap()
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(server.cert_resolver.clone());
|
||||
assert!(handshake(Arc::new(client), Arc::new(server)).is_err());
|
||||
|
||||
let (client, server) = configs("X25519", hybrid);
|
||||
let client = rustls::ClientConfig::builder_with_provider(client.crypto_provider().clone())
|
||||
.with_protocol_versions(&[&rustls::version::TLS12])
|
||||
.unwrap()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(get_disabled_verifier())
|
||||
.with_no_client_auth();
|
||||
assert!(handshake(Arc::new(client), Arc::new(server)).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_order_hello_retry_and_classical_fallback() {
|
||||
let (client, server) = configs("X25519MLKEM768, SecP256r1MLKEM768", "SecP256r1MLKEM768");
|
||||
let (client, server) = handshake(Arc::new(client), Arc::new(server)).unwrap();
|
||||
assert_group(&client, &server, NamedGroup::secp256r1MLKEM768);
|
||||
assert_eq!(
|
||||
client.handshake_kind(),
|
||||
Some(HandshakeKind::FullWithHelloRetryRequest)
|
||||
);
|
||||
for (client, server) in [
|
||||
("X25519MLKEM768, X25519", "X25519"),
|
||||
("X25519", "X25519MLKEM768, X25519"),
|
||||
("", "X25519"),
|
||||
("X25519", ""),
|
||||
] {
|
||||
let (client, server) = configs(client, server);
|
||||
let (client, server) = handshake(Arc::new(client), Arc::new(server)).unwrap();
|
||||
assert_group(&client, &server, NamedGroup::X25519);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_selection_is_local_and_ordered() {
|
||||
let original: Vec<_> = get_crypto_provider()
|
||||
.kx_groups
|
||||
.iter()
|
||||
.map(|g| g.name())
|
||||
.collect();
|
||||
let (client, server) = configs("SecP256r1MLKEM768, X25519MLKEM768", "SecP256r1MLKEM768");
|
||||
assert_eq!(
|
||||
client
|
||||
.crypto_provider()
|
||||
.kx_groups
|
||||
.iter()
|
||||
.map(|g| g.name())
|
||||
.collect::<Vec<_>>(),
|
||||
[NamedGroup::secp256r1MLKEM768, NamedGroup::X25519MLKEM768]
|
||||
);
|
||||
assert_eq!(server.crypto_provider().kx_groups.len(), 1);
|
||||
for provider in [
|
||||
get_crypto_provider(),
|
||||
create_dns_client_config().crypto_provider().clone(),
|
||||
try_create_client_config(false, vec![], vec![], true, None, true, &Default::default())
|
||||
.unwrap()
|
||||
.crypto_provider()
|
||||
.clone(),
|
||||
] {
|
||||
assert_eq!(
|
||||
provider
|
||||
.kx_groups
|
||||
.iter()
|
||||
.map(|g| g.name())
|
||||
.collect::<Vec<_>>(),
|
||||
original
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hybrid_exchange_preserves_mutual_authentication_and_pinning() {
|
||||
for group in ["X25519MLKEM768", "SecP256r1MLKEM768"] {
|
||||
let server_cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap();
|
||||
let client_cert = rcgen::generate_simple_self_signed(vec!["client".into()]).unwrap();
|
||||
for (client_auth, pin_matches, success) in [
|
||||
(true, true, true),
|
||||
(false, true, false),
|
||||
(true, false, false),
|
||||
] {
|
||||
let client = try_create_client_config(
|
||||
false,
|
||||
vec![if pin_matches {
|
||||
fingerprint(&server_cert.cert)
|
||||
} else {
|
||||
"00".repeat(32)
|
||||
}],
|
||||
vec![],
|
||||
true,
|
||||
client_auth.then(|| {
|
||||
(
|
||||
client_cert.signing_key.serialize_pem().into_bytes(),
|
||||
client_cert.cert.pem().into_bytes(),
|
||||
)
|
||||
}),
|
||||
false,
|
||||
&configured_groups(group),
|
||||
)
|
||||
.unwrap();
|
||||
let server = try_create_server_config(
|
||||
server_cert.cert.pem().as_bytes(),
|
||||
server_cert.signing_key.serialize_pem().as_bytes(),
|
||||
vec![],
|
||||
&[],
|
||||
&[fingerprint(&client_cert.cert)],
|
||||
&configured_groups(group),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
handshake(Arc::new(client), Arc::new(server)).is_ok(),
|
||||
success
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_and_mixed_lists_preserve_tls12_compatibility() {
|
||||
for configured in ["", "X25519MLKEM768, X25519"] {
|
||||
let (client, server) = configs(configured, "X25519");
|
||||
let mut server =
|
||||
rustls::ServerConfig::builder_with_provider(server.crypto_provider().clone())
|
||||
.with_protocol_versions(&[&rustls::version::TLS12])
|
||||
.unwrap()
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(server.cert_resolver.clone());
|
||||
server.alpn_protocols = vec![b"hybrid-test".to_vec()];
|
||||
let (client, server) = handshake(Arc::new(client), Arc::new(server)).unwrap();
|
||||
assert_eq!(
|
||||
client.protocol_version(),
|
||||
Some(rustls::ProtocolVersion::TLSv1_2)
|
||||
);
|
||||
assert_eq!(
|
||||
server.protocol_version(),
|
||||
Some(rustls::ProtocolVersion::TLSv1_2)
|
||||
);
|
||||
|
||||
let (client, server) = configs("X25519", configured);
|
||||
let mut client =
|
||||
rustls::ClientConfig::builder_with_provider(client.crypto_provider().clone())
|
||||
.with_protocol_versions(&[&rustls::version::TLS12])
|
||||
.unwrap()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(get_disabled_verifier())
|
||||
.with_no_client_auth();
|
||||
client.alpn_protocols = vec![b"hybrid-test".to_vec()];
|
||||
let (client, server) = handshake(Arc::new(client), Arc::new(server)).unwrap();
|
||||
assert_eq!(
|
||||
client.protocol_version(),
|
||||
Some(rustls::ProtocolVersion::TLSv1_2)
|
||||
);
|
||||
assert_eq!(
|
||||
server.protocol_version(),
|
||||
Some(rustls::ProtocolVersion::TLSv1_2)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn crypto_stream_handles_fragmented_hybrid_handshakes() {
|
||||
use crate::crypto::{CryptoConnection, CryptoTlsStream, TlsReadMode};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
tokio::time::timeout(std::time::Duration::from_secs(5), async {
|
||||
for name in ["X25519MLKEM768", "SecP256r1MLKEM768"] {
|
||||
let (client, mut server) = configs(name, name);
|
||||
// Keep this tiny duplex focused on handshake fragmentation, not post-handshake tickets.
|
||||
server.send_tls13_tickets = 0;
|
||||
let client = CryptoConnection::new_rustls_client(
|
||||
ClientConnection::new(Arc::new(client), "localhost".try_into().unwrap()).unwrap(),
|
||||
);
|
||||
let server = CryptoConnection::new_rustls_server(
|
||||
ServerConnection::new(Arc::new(server)).unwrap(),
|
||||
);
|
||||
let (client_io, server_io) = tokio::io::duplex(43);
|
||||
let (mut client, mut server) = tokio::try_join!(
|
||||
CryptoTlsStream::handshake(client_io, client, TlsReadMode::Stream, &[]),
|
||||
CryptoTlsStream::handshake(server_io, server, TlsReadMode::Stream, &[]),
|
||||
)
|
||||
.unwrap();
|
||||
let send = async {
|
||||
client.write_all(b"hybrid").await.unwrap();
|
||||
client.flush().await.unwrap();
|
||||
};
|
||||
let receive = async {
|
||||
let mut data = [0; 6];
|
||||
server.read_exact(&mut data).await.unwrap();
|
||||
assert_eq!(&data, b"hybrid");
|
||||
};
|
||||
tokio::join!(send, receive);
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn quic_hybrid_data_and_disjoint_group_rejection() {
|
||||
tokio::time::timeout(std::time::Duration::from_secs(10), async {
|
||||
for (client_name, server_name, success) in [
|
||||
("X25519MLKEM768", "X25519MLKEM768", true),
|
||||
("SecP256r1MLKEM768", "SecP256r1MLKEM768", true),
|
||||
("X25519MLKEM768", "X25519", false),
|
||||
("X25519", "SecP256r1MLKEM768", false),
|
||||
("", "X25519", true),
|
||||
] {
|
||||
let (client, server) = configs(client_name, server_name);
|
||||
let server = quinn::crypto::rustls::QuicServerConfig::try_from(server).unwrap();
|
||||
let server = quinn::Endpoint::server(
|
||||
quinn::ServerConfig::with_crypto(Arc::new(server)),
|
||||
"0.0.0.0:0".parse().unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let client = {
|
||||
let config = quinn::crypto::rustls::QuicClientConfig::try_from(client).unwrap();
|
||||
let mut endpoint = quinn::Endpoint::client("0.0.0.0:0".parse().unwrap()).unwrap();
|
||||
endpoint.set_default_client_config(quinn::ClientConfig::new(Arc::new(config)));
|
||||
endpoint
|
||||
};
|
||||
let target = (
|
||||
std::net::Ipv4Addr::LOCALHOST,
|
||||
server.local_addr().unwrap().port(),
|
||||
)
|
||||
.into();
|
||||
let connect = client.connect(target, "localhost").unwrap();
|
||||
let accept = async { server.accept().await.unwrap().await };
|
||||
let (outbound, inbound) = tokio::join!(connect, accept);
|
||||
assert_eq!(outbound.is_ok(), success);
|
||||
assert_eq!(inbound.is_ok(), success);
|
||||
if success {
|
||||
let outbound = outbound.unwrap();
|
||||
let inbound = inbound.unwrap();
|
||||
let mut send = outbound.open_uni().await.unwrap();
|
||||
send.write_all(b"quic hybrid").await.unwrap();
|
||||
send.finish().unwrap();
|
||||
let mut receive = inbound.accept_uni().await.unwrap();
|
||||
assert_eq!(receive.read_to_end(1024).await.unwrap(), b"quic hybrid");
|
||||
}
|
||||
client.close(0u32.into(), b"done");
|
||||
server.close(0u32.into(), b"done");
|
||||
client.wait_idle().await;
|
||||
server.wait_idle().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -66,6 +66,12 @@ fn build_client_proxy_chain_with_limits(
|
||||
));
|
||||
}
|
||||
|
||||
for (index, configs) in hops.iter().enumerate() {
|
||||
for config in configs {
|
||||
config.validate_quic_key_exchange(index == 0)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Build initial hop entries from hop 0.
|
||||
// Each entry pairs socket + optional proxy together to ensure atomic selection.
|
||||
let initial_hop: Vec<InitialHopEntry> = hops[0]
|
||||
@@ -234,6 +240,50 @@ mod tests {
|
||||
ClientConfig::default()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quic_allowlists_fail_before_ignored_transport_construction() {
|
||||
for groups in [None, Some("X25519MLKEM768"), Some("X25519")] {
|
||||
let field = groups
|
||||
.map(|g| format!("key_exchange_groups: [{g}]"))
|
||||
.unwrap_or_default();
|
||||
let direct: ClientConfig = serde_yaml::from_str(&format!(
|
||||
"protocol: {{type: direct}}\ntransport: quic\nquic_settings: {{{field}}}\n"
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
SocketConnectorImpl::from_config(&direct, None).is_err(),
|
||||
groups.is_some()
|
||||
);
|
||||
let direct_chain =
|
||||
OneOrSome::One(ClientChainHop::Single(ConfigSelection::Config(direct)));
|
||||
assert_eq!(
|
||||
try_build_client_proxy_chain(direct_chain, mock_resolver()).is_err(),
|
||||
groups.is_some()
|
||||
);
|
||||
|
||||
let proxy: ClientConfig = serde_yaml::from_str(&format!(
|
||||
"address: '127.0.0.1:443'\nprotocol: {{type: socks}}\ntransport: quic\nquic_settings: {{{field}}}\n"
|
||||
)).unwrap();
|
||||
for hop in [
|
||||
ClientChainHop::Single(ConfigSelection::Config(proxy.clone())),
|
||||
ClientChainHop::Pool(OneOrSome::Some(vec![
|
||||
ConfigSelection::Config(socks_config(1080)),
|
||||
ConfigSelection::Config(proxy),
|
||||
])),
|
||||
] {
|
||||
let chain = OneOrSome::Some(vec![
|
||||
ClientChainHop::Single(ConfigSelection::Config(direct_config())),
|
||||
hop,
|
||||
]);
|
||||
let result = try_build_client_proxy_chain(chain, mock_resolver());
|
||||
assert_eq!(result.is_err(), groups.is_some());
|
||||
if let Err(error) = result {
|
||||
assert!(error.to_string().contains("QUIC key_exchange_groups"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_single_direct_hop() {
|
||||
let chain = build_client_proxy_chain(
|
||||
|
||||
@@ -154,6 +154,7 @@ impl SocketConnectorImpl {
|
||||
target_address: Option<&NetLocation>,
|
||||
limits: crate::config::GlobalLimits,
|
||||
) -> std::io::Result<Self> {
|
||||
config.validate_quic_key_exchange(true)?;
|
||||
let bind_interface = config.bind_interface.clone().into_option();
|
||||
|
||||
let default_sni_hostname =
|
||||
@@ -185,6 +186,7 @@ impl SocketConnectorImpl {
|
||||
})?;
|
||||
|
||||
let ClientQuicConfig {
|
||||
key_exchange_groups,
|
||||
verify,
|
||||
server_fingerprints,
|
||||
alpn_protocols,
|
||||
@@ -226,6 +228,7 @@ impl SocketConnectorImpl {
|
||||
sni_hostname.is_some(),
|
||||
key_and_cert_bytes,
|
||||
false, // tls13_only - QUIC enforces TLS 1.3 anyway
|
||||
&key_exchange_groups,
|
||||
)?;
|
||||
|
||||
let quic_client_config = quinn::crypto::rustls::QuicClientConfig::with_initial(
|
||||
|
||||
@@ -126,6 +126,7 @@ pub fn create_tcp_client_handler(
|
||||
}
|
||||
ClientProxyConfig::Tls(tls_client_config) => {
|
||||
let TlsClientConfig {
|
||||
key_exchange_groups,
|
||||
verify,
|
||||
server_fingerprints,
|
||||
sni_hostname,
|
||||
@@ -136,6 +137,7 @@ pub fn create_tcp_client_handler(
|
||||
cert,
|
||||
vision,
|
||||
} = tls_client_config;
|
||||
key_exchange_groups.validate_vision(vision)?;
|
||||
|
||||
let sni_hostname = if sni_hostname.is_unspecified() {
|
||||
if let Some(ref hostname) = default_sni_hostname {
|
||||
@@ -164,6 +166,7 @@ pub fn create_tcp_client_handler(
|
||||
sni_hostname.is_some(),
|
||||
key_and_cert_bytes,
|
||||
false, // tls13_only
|
||||
&key_exchange_groups,
|
||||
)?);
|
||||
|
||||
let server_name = match sni_hostname {
|
||||
@@ -288,6 +291,7 @@ pub fn create_tcp_client_handler(
|
||||
enable_sni, // Enable SNI if hostname provided
|
||||
None, // No client cert
|
||||
true, // tls13_only - required for ShadowTLS v3
|
||||
&Default::default(),
|
||||
)?);
|
||||
|
||||
let handler = create_tcp_client_handler(*protocol, None, resolver.clone())?;
|
||||
|
||||
@@ -312,6 +312,7 @@ fn create_tls_server_target(
|
||||
bind_ip: Option<IpAddr>,
|
||||
) -> std::io::Result<TlsServerTarget> {
|
||||
let TlsServerConfig {
|
||||
key_exchange_groups,
|
||||
cert,
|
||||
key,
|
||||
alpn_protocols,
|
||||
@@ -321,6 +322,7 @@ fn create_tls_server_target(
|
||||
protocol,
|
||||
override_rules,
|
||||
} = tls_server_config;
|
||||
key_exchange_groups.validate_vision(vision)?;
|
||||
|
||||
if vision && !matches!(protocol, ServerProxyConfig::Vless { .. }) {
|
||||
return Err(std::io::Error::new(
|
||||
@@ -360,6 +362,7 @@ fn create_tls_server_target(
|
||||
client_ca_certs,
|
||||
&effective_alpn,
|
||||
&client_fingerprints.into_vec(),
|
||||
&key_exchange_groups,
|
||||
)?);
|
||||
|
||||
// Compute effective selector: if override_rules exist, create new selector; otherwise use parent's
|
||||
@@ -438,6 +441,7 @@ fn create_shadow_tls_server_target(
|
||||
client_ca_certs,
|
||||
&handshake.alpn_protocols.into_vec(),
|
||||
&handshake.client_fingerprints.into_vec(),
|
||||
&Default::default(),
|
||||
)?);
|
||||
|
||||
ShadowTlsServerTargetHandshake::new_local(server_config)
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
use std::{io, sync::Arc, time::Duration};
|
||||
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};
|
||||
use shoes_test_support::{port_helper::PortHelper, test_fixture::start_shoes_server};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
|
||||
fn provider(name: &str) -> Arc<rustls::crypto::CryptoProvider> {
|
||||
use rustls::crypto::aws_lc_rs::{default_provider, kx_group};
|
||||
let mut provider = default_provider();
|
||||
provider.kx_groups = vec![match name {
|
||||
"X25519MLKEM768" => kx_group::X25519MLKEM768,
|
||||
"SecP256r1MLKEM768" => kx_group::SECP256R1MLKEM768,
|
||||
"X25519" => kx_group::X25519,
|
||||
_ => unreachable!(),
|
||||
}];
|
||||
Arc::new(provider)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tls_server_named_and_default_targets_enforce_groups() -> io::Result<()> {
|
||||
let scenario = async {
|
||||
let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap();
|
||||
let mut roots = rustls::RootCertStore::empty();
|
||||
roots.add(cert.cert.der().clone()).unwrap();
|
||||
let echo =
|
||||
shoes_test_support::test_servers::start_tcp_stream_echo_server("0.0.0.0", 0).await?;
|
||||
for group in ["X25519MLKEM768", "SecP256r1MLKEM768"] {
|
||||
for default_target in [false, true] {
|
||||
let mut ports = PortHelper::new();
|
||||
let (_, port) = ports.get_localhost_listener_port();
|
||||
let target = serde_json::json!({
|
||||
"cert": cert.cert.pem(), "key": cert.signing_key.serialize_pem(),
|
||||
"key_exchange_groups": [group],
|
||||
"protocol": {"type": "forward", "target": format!("127.0.0.1:{}", echo.local_addr().port())}
|
||||
});
|
||||
let mut protocol = serde_json::json!({"type": "tls"});
|
||||
if default_target {
|
||||
protocol["default_tls_target"] = target;
|
||||
} else {
|
||||
protocol["tls_targets"] = serde_json::json!({"localhost": target});
|
||||
}
|
||||
let config = serde_json::json!([{"address": format!("0.0.0.0:{port}"), "protocol": protocol}]);
|
||||
let _shoes = start_shoes_server(&serde_yaml::to_string(&config).unwrap())?;
|
||||
ports.wait_for_all_ports().await?;
|
||||
for peer_group in [group, "X25519"] {
|
||||
let provider = provider(peer_group);
|
||||
let expected = provider.kx_groups[0].name();
|
||||
let client = rustls::ClientConfig::builder_with_provider(provider)
|
||||
.with_protocol_versions(&[&rustls::version::TLS13])
|
||||
.unwrap()
|
||||
.with_root_certificates(roots.clone())
|
||||
.with_no_client_auth();
|
||||
let result = tokio_rustls::TlsConnector::from(Arc::new(client))
|
||||
.connect(
|
||||
"localhost".try_into().unwrap(),
|
||||
TcpStream::connect(("127.0.0.1", port)).await?,
|
||||
)
|
||||
.await;
|
||||
if peer_group == "X25519" {
|
||||
assert!(result.is_err(), "server ignored {group} allowlist");
|
||||
continue;
|
||||
}
|
||||
let mut stream = result?;
|
||||
assert_eq!(
|
||||
stream
|
||||
.get_ref()
|
||||
.1
|
||||
.negotiated_key_exchange_group()
|
||||
.unwrap()
|
||||
.name(),
|
||||
expected
|
||||
);
|
||||
let payload = vec![0x5a; 64 * 1024];
|
||||
stream.write_all(&payload).await?;
|
||||
stream.flush().await?;
|
||||
let mut response = vec![0; payload.len()];
|
||||
stream.read_exact(&mut response).await?;
|
||||
assert_eq!(payload, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
tokio::time::timeout(Duration::from_secs(30), scenario).await?
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tls_client_enforces_groups() -> io::Result<()> {
|
||||
let scenario = async {
|
||||
let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap();
|
||||
let fingerprint = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, cert.cert.der());
|
||||
let fingerprint: String = fingerprint
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|b| format!("{b:02x}"))
|
||||
.collect();
|
||||
for group in ["X25519MLKEM768", "SecP256r1MLKEM768"] {
|
||||
for peer_group in [group, "X25519"] {
|
||||
let listener = TcpListener::bind("0.0.0.0:0").await?;
|
||||
let peer_port = listener.local_addr()?.port();
|
||||
let provider = provider(peer_group);
|
||||
let expected = provider.kx_groups[0].name();
|
||||
let server = rustls::ServerConfig::builder_with_provider(provider)
|
||||
.with_protocol_versions(&[&rustls::version::TLS13])
|
||||
.unwrap()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(
|
||||
vec![CertificateDer::from_pem_slice(cert.cert.pem().as_bytes()).unwrap()],
|
||||
PrivateKeyDer::from_pem_slice(cert.signing_key.serialize_pem().as_bytes())
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut ports = PortHelper::new();
|
||||
let (_, port) = ports.get_localhost_listener_port();
|
||||
let config = serde_json::json!([{
|
||||
"address": format!("0.0.0.0:{port}"),
|
||||
"protocol": {"type": "http"},
|
||||
"rules": [{"mask": "0.0.0.0/0", "action": "allow", "client_chain": [{
|
||||
"address": format!("127.0.0.1:{peer_port}"),
|
||||
"protocol": {"type": "tls", "verify": false, "server_fingerprints": [fingerprint],
|
||||
"sni_hostname": "localhost", "key_exchange_groups": [group], "protocol": {"type": "portforward"}}
|
||||
}]}]
|
||||
}]);
|
||||
let _shoes = start_shoes_server(&serde_yaml::to_string(&config).unwrap())?;
|
||||
ports.wait_for_all_ports().await?;
|
||||
let client = async {
|
||||
let mut stream = TcpStream::connect(("127.0.0.1", port)).await?;
|
||||
stream
|
||||
.write_all(b"CONNECT 127.0.0.1:80 HTTP/1.1\r\nHost: localhost\r\n\r\n")
|
||||
.await?;
|
||||
let mut header = Vec::new();
|
||||
while !header.ends_with(b"\r\n\r\n") {
|
||||
match stream.read_u8().await {
|
||||
Ok(byte) => header.push(byte),
|
||||
Err(_) if peer_group == "X25519" => return Ok(()),
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
assert!(header.len() < 4096);
|
||||
}
|
||||
if peer_group == "X25519" {
|
||||
assert!(!header.starts_with(b"HTTP/1.1 200"));
|
||||
return Ok(());
|
||||
}
|
||||
assert!(header.starts_with(b"HTTP/1.1 200"));
|
||||
stream.write_all(b"hybrid client").await?;
|
||||
let mut response = [0; 13];
|
||||
stream.read_exact(&mut response).await?;
|
||||
assert_eq!(&response, b"hybrid client");
|
||||
io::Result::Ok(())
|
||||
};
|
||||
let server = async {
|
||||
let (socket, _) = listener.accept().await?;
|
||||
let result = tokio_rustls::TlsAcceptor::from(Arc::new(server))
|
||||
.accept(socket)
|
||||
.await;
|
||||
if peer_group == "X25519" {
|
||||
assert!(result.is_err(), "client ignored {group} allowlist");
|
||||
return Ok(());
|
||||
}
|
||||
let mut stream = result?;
|
||||
assert_eq!(
|
||||
stream
|
||||
.get_ref()
|
||||
.1
|
||||
.negotiated_key_exchange_group()
|
||||
.unwrap()
|
||||
.name(),
|
||||
expected
|
||||
);
|
||||
let mut payload = [0; 13];
|
||||
stream.read_exact(&mut payload).await?;
|
||||
stream.write_all(&payload).await?;
|
||||
stream.shutdown().await?;
|
||||
io::Result::Ok(())
|
||||
};
|
||||
tokio::try_join!(client, server)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
tokio::time::timeout(Duration::from_secs(30), scenario).await?
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hybrid_quic_and_tls_udp_over_tcp() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let scenario = async {
|
||||
let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap();
|
||||
let echo = shoes_test_support::test_servers::start_udp_echo_server("0.0.0.0", 0).await?;
|
||||
for transport in ["tcp", "quic"] {
|
||||
for group in ["X25519MLKEM768", "SecP256r1MLKEM768"] {
|
||||
let mut ports = PortHelper::new();
|
||||
let (_, server_port) = if transport == "quic" {
|
||||
ports.get_quic_listener_port()
|
||||
} else {
|
||||
ports.get_localhost_listener_port()
|
||||
};
|
||||
let (_, client_port) = ports.get_localhost_listener_port();
|
||||
let inner = serde_json::json!({"type": "vless", "user_id": "550e8400-e29b-41d4-a716-446655440000"});
|
||||
let server_tls = serde_json::json!({"cert": cert.cert.pem(), "key": cert.signing_key.serialize_pem(), "key_exchange_groups": [group]});
|
||||
let client_tls = serde_json::json!({"verify": false, "sni_hostname": "localhost", "key_exchange_groups": [group]});
|
||||
let mut server = serde_json::json!({"address": format!("0.0.0.0:{server_port}"), "transport": transport});
|
||||
let mut outbound = serde_json::json!({"address": format!("127.0.0.1:{server_port}"), "transport": transport});
|
||||
if transport == "quic" {
|
||||
server["quic_settings"] = server_tls;
|
||||
server["protocol"] = inner.clone();
|
||||
outbound["quic_settings"] = client_tls;
|
||||
outbound["protocol"] = inner;
|
||||
} else {
|
||||
let mut target = server_tls;
|
||||
target["protocol"] = inner.clone();
|
||||
server["protocol"] =
|
||||
serde_json::json!({"type": "tls", "default_tls_target": target});
|
||||
let mut client = client_tls;
|
||||
client["type"] = "tls".into();
|
||||
client["protocol"] = inner;
|
||||
outbound["protocol"] = client;
|
||||
}
|
||||
let config = serde_json::json!([server, {
|
||||
"address": format!("0.0.0.0:{client_port}"), "protocol": {"type": "socks"},
|
||||
"rules": [{"mask": "0.0.0.0/0", "action": "allow", "client_chain": [outbound]}]
|
||||
}]);
|
||||
let _shoes = start_shoes_server(&serde_yaml::to_string(&config)?)?;
|
||||
ports.wait_for_all_ports().await?;
|
||||
let association = shoes_test_support::socks5::Socks5UdpAssociation::connect(
|
||||
"127.0.0.1",
|
||||
client_port,
|
||||
)
|
||||
.await?;
|
||||
let target = ([127, 0, 0, 1], echo.local_addr().port()).into();
|
||||
let response = association.send_to(target, b"hybrid datagram").await?;
|
||||
assert_eq!(response, b"hybrid datagram [ECHO]");
|
||||
}
|
||||
}
|
||||
Ok::<_, Box<dyn std::error::Error>>(())
|
||||
};
|
||||
tokio::time::timeout(Duration::from_secs(40), scenario).await?
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
use std::{io, io::Write, process::Command, time::Duration};
|
||||
|
||||
use rustls::pki_types::{CertificateDer, pem::PemObject};
|
||||
use serde_json::json;
|
||||
use shoes_test_support::{
|
||||
certs::generate_test_cert_files, port_helper::PortHelper, process::ProcessGuard,
|
||||
test_fixture::start_shoes_server, test_servers::start_tcp_stream_echo_server,
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::TcpStream,
|
||||
};
|
||||
|
||||
fn start_xray(config: serde_json::Value) -> io::Result<(ProcessGuard, tempfile::NamedTempFile)> {
|
||||
let binary = std::env::var_os("SHOES_TEST_XRAY_BIN").unwrap_or_else(|| "xray".into());
|
||||
let mut file = tempfile::NamedTempFile::new()?;
|
||||
file.write_all(config.to_string().as_bytes())?;
|
||||
file.flush()?;
|
||||
let child = Command::new(binary)
|
||||
.args(["run", "-format", "json", "-config"])
|
||||
.arg(file.path())
|
||||
.env("GOMAXPROCS", "1")
|
||||
.spawn()?;
|
||||
Ok((ProcessGuard::new(child, "xray hybrid TLS peer"), file))
|
||||
}
|
||||
|
||||
async fn echo_through_http(port: u16, target_port: u16) -> io::Result<()> {
|
||||
let mut stream = TcpStream::connect(("127.0.0.1", port)).await?;
|
||||
stream
|
||||
.write_all(
|
||||
format!(
|
||||
"CONNECT 127.0.0.1:{target_port} HTTP/1.1\r\nHost: 127.0.0.1:{target_port}\r\n\r\n"
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
let mut headers = Vec::new();
|
||||
while !headers.ends_with(b"\r\n\r\n") {
|
||||
headers.push(stream.read_u8().await?);
|
||||
assert!(headers.len() < 4096);
|
||||
}
|
||||
assert!(
|
||||
headers.starts_with(b"HTTP/1.1 200"),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&headers)
|
||||
);
|
||||
let payload: Vec<_> = (0..65537).map(|i| (i % 251) as u8).collect();
|
||||
stream.write_all(&payload).await?;
|
||||
let mut response = vec![0; payload.len()];
|
||||
stream.read_exact(&mut response).await?;
|
||||
assert_eq!(payload, response);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn xray_both_native_hybrids_in_both_directions() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let scenario = async {
|
||||
let (cert_path, key_path) = generate_test_cert_files()?;
|
||||
let pem = std::fs::read(&cert_path)?;
|
||||
let cert = CertificateDer::from_pem_slice(&pem)?;
|
||||
let fingerprint = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, &cert);
|
||||
let fingerprint: String = fingerprint
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|b| format!("{b:02x}"))
|
||||
.collect();
|
||||
let echo = start_tcp_stream_echo_server("0.0.0.0", 0).await?;
|
||||
for group in ["X25519MLKEM768", "SecP256r1MLKEM768"] {
|
||||
for shoes_is_server in [false, true] {
|
||||
let mut ports = PortHelper::new();
|
||||
let (_, server_port) = ports.get_localhost_listener_port();
|
||||
let (_, client_port) = ports.get_localhost_listener_port();
|
||||
// Xray's "unsafe" fingerprint selects native Go TLS, not a uTLS preset
|
||||
// that can override curve preferences. Certificate pinning stays enabled.
|
||||
let (shoes_config, xray_config) = if shoes_is_server {
|
||||
(
|
||||
json!([{
|
||||
"address": format!("0.0.0.0:{server_port}"),
|
||||
"protocol": {"type": "tls", "default_tls_target": {
|
||||
"cert": cert_path.to_str().unwrap(), "key": key_path.to_str().unwrap(),
|
||||
"key_exchange_groups": [group], "protocol": {"type": "http"}
|
||||
}}
|
||||
}]),
|
||||
json!({
|
||||
"log": {"loglevel": "warning"},
|
||||
"inbounds": [{"listen": "0.0.0.0", "port": client_port, "protocol": "http", "settings": {}}],
|
||||
"outbounds": [{"protocol": "http", "settings": {"servers": [{"address": "127.0.0.1", "port": server_port}]},
|
||||
"streamSettings": {"network": "tcp", "security": "tls", "tlsSettings": {
|
||||
"serverName": "test.local", "pinnedPeerCertSha256": fingerprint,
|
||||
"minVersion": "1.3", "curvePreferences": [group], "fingerprint": "unsafe"
|
||||
}}}]
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
json!([{
|
||||
"address": format!("0.0.0.0:{client_port}"), "protocol": {"type": "http"},
|
||||
"rules": [{"mask": "0.0.0.0/0", "action": "allow", "client_chain": [{
|
||||
"address": format!("127.0.0.1:{server_port}"),
|
||||
"protocol": {"type": "tls", "verify": false, "server_fingerprints": [fingerprint], "sni_hostname": "test.local",
|
||||
"key_exchange_groups": [group], "protocol": {"type": "http"}}
|
||||
}]}]
|
||||
}]),
|
||||
json!({
|
||||
"log": {"loglevel": "warning"},
|
||||
"inbounds": [{"listen": "0.0.0.0", "port": server_port, "protocol": "http", "settings": {},
|
||||
"streamSettings": {"network": "tcp", "security": "tls", "tlsSettings": {
|
||||
"minVersion": "1.3", "curvePreferences": [group],
|
||||
"certificates": [{"certificateFile": cert_path.to_str().unwrap(), "keyFile": key_path.to_str().unwrap()}]
|
||||
}}}],
|
||||
"outbounds": [{"protocol": "freedom", "settings": {"finalRules": [{"action": "allow", "ip": ["127.0.0.0/8"]}]}}]
|
||||
}),
|
||||
)
|
||||
};
|
||||
let _shoes = start_shoes_server(&serde_yaml::to_string(&shoes_config)?)?;
|
||||
let _xray = start_xray(xray_config)?;
|
||||
ports.wait_for_all_ports().await?;
|
||||
echo_through_http(client_port, echo.local_addr().port()).await?;
|
||||
}
|
||||
}
|
||||
Ok::<_, Box<dyn std::error::Error>>(())
|
||||
};
|
||||
tokio::time::timeout(Duration::from_secs(45), scenario).await?
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn singbox_hybrid_quic_tuic_and_hysteria2() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let scenario = async {
|
||||
let (cert_path, key_path) = generate_test_cert_files()?;
|
||||
let echo = start_tcp_stream_echo_server("0.0.0.0", 0).await?;
|
||||
for protocol in ["tuic", "hysteria2"] {
|
||||
let mut ports = PortHelper::new();
|
||||
let (_, server_port) = ports.get_quic_listener_port();
|
||||
let (_, client_port) = ports.get_localhost_listener_port();
|
||||
let uuid = "550e8400-e29b-41d4-a716-446655440000";
|
||||
let mut inner = json!({"type": protocol, "password": "hybrid-test"});
|
||||
if protocol == "tuic" {
|
||||
inner["uuid"] = uuid.into();
|
||||
}
|
||||
let shoes = json!([{
|
||||
"address": format!("0.0.0.0:{server_port}"), "transport": "quic", "protocol": inner,
|
||||
"quic_settings": {"cert": cert_path.to_str().unwrap(), "key": key_path.to_str().unwrap(),
|
||||
"alpn_protocols": ["h3"], "num_endpoints": 1, "key_exchange_groups": ["X25519MLKEM768"]}
|
||||
}]);
|
||||
let mut outbound = json!({"type": protocol, "tag": "peer", "server": "127.0.0.1", "server_port": server_port, "password": "hybrid-test",
|
||||
"tls": {"enabled": true, "insecure": true, "alpn": ["h3"], "curve_preferences": ["X25519MLKEM768"]}});
|
||||
if protocol == "tuic" {
|
||||
outbound["uuid"] = uuid.into();
|
||||
}
|
||||
let singbox = json!({"log": {"level": "warn"}, "inbounds": [{"type": "http", "listen": "0.0.0.0", "listen_port": client_port}],
|
||||
"outbounds": [outbound], "route": {"final": "peer"}});
|
||||
let _shoes = start_shoes_server(&serde_yaml::to_string(&shoes)?)?;
|
||||
let _singbox =
|
||||
shoes_test_support::test_fixture::start_singbox_server(&singbox.to_string())?;
|
||||
ports.wait_for_all_ports().await?;
|
||||
echo_through_http(client_port, echo.local_addr().port()).await?;
|
||||
}
|
||||
Ok::<_, Box<dyn std::error::Error>>(())
|
||||
};
|
||||
tokio::time::timeout(Duration::from_secs(30), scenario).await?
|
||||
}
|
||||
@@ -40,6 +40,8 @@ mod socks;
|
||||
mod socks5_udp_associate;
|
||||
#[path = "cases/integration/socks_uot_integration.rs"]
|
||||
mod socks_uot;
|
||||
#[path = "cases/integration/tls_hybrid.rs"]
|
||||
mod tls_hybrid;
|
||||
#[path = "cases/integration/tuic_integration.rs"]
|
||||
mod tuic;
|
||||
#[path = "cases/integration/tuic_udp_integration.rs"]
|
||||
|
||||
@@ -3,3 +3,6 @@ mod anytls_uot_mihomo;
|
||||
|
||||
#[path = "cases/optional_peer/snell_mihomo.rs"]
|
||||
mod snell_mihomo;
|
||||
|
||||
#[path = "cases/optional_peer/tls_hybrid.rs"]
|
||||
mod tls_hybrid;
|
||||
|
||||
@@ -441,6 +441,33 @@ async fn check_alias_and_validation_exit_codes() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unsupported_quic_allowlists_fail_validation_and_start() {
|
||||
use serde_json::json;
|
||||
let groups = json!({"key_exchange_groups": ["X25519MLKEM768"]});
|
||||
let direct = json!({"protocol": {"type": "direct"}});
|
||||
let guarded_direct =
|
||||
json!({"protocol": {"type": "direct"}, "transport": "quic", "quic_settings": groups});
|
||||
let guarded_proxy = json!({"address": "127.0.0.1:443", "protocol": {"type": "portforward"}, "transport": "quic", "quic_settings": groups});
|
||||
for chain in [
|
||||
json!([guarded_direct]),
|
||||
json!([direct, guarded_proxy]),
|
||||
json!([direct, {"pool": ["guarded"]}]),
|
||||
] {
|
||||
let config = json!([
|
||||
{"client_group": "guarded", "client_proxies": [guarded_proxy]},
|
||||
{"address": "0.0.0.0:0", "protocol": {"type": "http"}, "rules": [{"mask": "0.0.0.0/0", "action": "allow", "client_chain": chain}]}
|
||||
]);
|
||||
let config_file = tempfile::NamedTempFile::new().unwrap();
|
||||
std::fs::write(config_file.path(), serde_yaml::to_string(&config).unwrap()).unwrap();
|
||||
for mode in ["--dry-run", "--no-reload"] {
|
||||
let mut process = Process::start(config_file.path(), &[mode]);
|
||||
process.wait_for("QUIC key_exchange_groups").await;
|
||||
assert_eq!(process.exit().await.code(), Some(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn initial_partial_bind_failure_exits_and_releases_addresses() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user