Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
Co-Authored-By: John Preston <17900494+john-preston@users.noreply.github.com>
This commit is contained in:
Alexey
2026-08-23 03:12:11 +03:00
parent 8dbd24b11b
commit 1029703c2c
58 changed files with 7460 additions and 2646 deletions
+70 -1
View File
@@ -11,6 +11,12 @@ pub(super) struct MtprotoCandidateValidation {
pub(super) encryptor: AesCtr,
}
#[derive(Clone, Copy)]
pub(super) enum MtprotoModePolicy {
Configured,
Web(WebSecretMode),
}
pub(super) fn sni_hint_hash(sni: &str) -> u64 {
let mut hasher = DefaultHasher::new();
for byte in sni.bytes() {
@@ -146,6 +152,7 @@ pub(super) fn validate_mtproto_secret_candidate(
secret: &[u8; ACCESS_SECRET_BYTES],
config: &ProxyConfig,
is_tls: bool,
mode_policy: MtprotoModePolicy,
) -> Option<MtprotoCandidateValidation> {
let mut dec_key_input = Zeroizing::new(Vec::with_capacity(PREKEY_LEN + secret.len()));
dec_key_input.extend_from_slice(dec_prekey);
@@ -163,7 +170,7 @@ pub(super) fn validate_mtproto_secret_candidate(
decrypted[PROTO_TAG_POS + 3],
];
let proto_tag = ProtoTag::from_bytes(tag_bytes)?;
if !mode_enabled_for_proto(config, proto_tag, is_tls) {
if !mode_enabled_for_proto_with_policy(config, proto_tag, is_tls, mode_policy) {
return None;
}
@@ -267,6 +274,28 @@ pub(super) fn mode_enabled_for_proto(
proto_tag: ProtoTag,
is_tls: bool,
) -> bool {
mode_enabled_for_proto_with_policy(
config,
proto_tag,
is_tls,
MtprotoModePolicy::Configured,
)
}
fn mode_enabled_for_proto_with_policy(
config: &ProxyConfig,
proto_tag: ProtoTag,
is_tls: bool,
policy: MtprotoModePolicy,
) -> bool {
if let MtprotoModePolicy::Web(secret_mode) = policy {
return match secret_mode {
WebSecretMode::Plain => {
matches!(proto_tag, ProtoTag::Intermediate | ProtoTag::Abridged)
}
WebSecretMode::Dd => matches!(proto_tag, ProtoTag::Secure),
};
}
match proto_tag {
ProtoTag::Secure => {
if is_tls {
@@ -279,6 +308,46 @@ pub(super) fn mode_enabled_for_proto(
}
}
#[cfg(test)]
mod web_mode_tests {
use super::*;
#[test]
fn web_secret_mode_isolates_inner_protocol_tags() {
let config = ProxyConfig::default();
assert!(mode_enabled_for_proto_with_policy(
&config,
ProtoTag::Abridged,
false,
MtprotoModePolicy::Web(WebSecretMode::Plain),
));
assert!(mode_enabled_for_proto_with_policy(
&config,
ProtoTag::Intermediate,
false,
MtprotoModePolicy::Web(WebSecretMode::Plain),
));
assert!(!mode_enabled_for_proto_with_policy(
&config,
ProtoTag::Secure,
false,
MtprotoModePolicy::Web(WebSecretMode::Plain),
));
assert!(mode_enabled_for_proto_with_policy(
&config,
ProtoTag::Secure,
false,
MtprotoModePolicy::Web(WebSecretMode::Dd),
));
assert!(!mode_enabled_for_proto_with_policy(
&config,
ProtoTag::Intermediate,
false,
MtprotoModePolicy::Web(WebSecretMode::Dd),
));
}
}
pub(super) fn decode_user_secrets_in(
shared: &ProxySharedState,
config: &ProxyConfig,
+63 -10
View File
@@ -1,6 +1,6 @@
use super::*;
/// Handle MTProto obfuscation handshake
/// Handles an MTProto obfuscation handshake with isolated test state.
#[cfg(test)]
pub async fn handle_mtproto_handshake<R, W>(
handshake: &[u8; HANDSHAKE_LEN],
@@ -26,11 +26,14 @@ where
replay_checker,
is_tls,
preferred_user,
None,
MtprotoModePolicy::Configured,
shared.as_ref(),
)
.await
}
/// Handles an MTProto obfuscation handshake with process-shared defenses.
pub async fn handle_mtproto_handshake_with_shared<R, W>(
handshake: &[u8; HANDSHAKE_LEN],
reader: R,
@@ -55,6 +58,40 @@ where
replay_checker,
is_tls,
preferred_user,
None,
MtprotoModePolicy::Configured,
shared,
)
.await
}
/// Authenticates one WEB logical stream against exactly one user and secret mode.
pub(crate) async fn handle_mtproto_handshake_for_web_user<R, W>(
handshake: &[u8; HANDSHAKE_LEN],
reader: R,
writer: W,
peer: SocketAddr,
config: &ProxyConfig,
replay_checker: &ReplayChecker,
exact_user: &str,
secret_mode: WebSecretMode,
shared: &ProxySharedState,
) -> HandshakeResult<(CryptoReader<R>, CryptoWriter<W>, HandshakeSuccess), R, W>
where
R: AsyncRead + Unpin + Send,
W: AsyncWrite + Unpin + Send,
{
handle_mtproto_handshake_impl(
handshake,
reader,
writer,
peer,
config,
replay_checker,
false,
None,
Some(exact_user),
MtprotoModePolicy::Web(secret_mode),
shared,
)
.await
@@ -69,6 +106,8 @@ async fn handle_mtproto_handshake_impl<R, W>(
replay_checker: &ReplayChecker,
is_tls: bool,
preferred_user: Option<&str>,
exact_user: Option<&str>,
mode_policy: MtprotoModePolicy,
shared: &ProxySharedState,
) -> HandshakeResult<(CryptoReader<R>, CryptoWriter<W>, HandshakeSuccess), R, W>
where
@@ -113,8 +152,11 @@ where
let sticky_ip_hint = sticky_hint_get_by_ip(shared, peer.ip());
let sticky_prefix_hint = sticky_hint_get_by_ip_prefix(shared, peer.ip());
let preferred_user_id = preferred_user.and_then(|user| snapshot.user_id_by_name(user));
let has_hint =
sticky_ip_hint.is_some() || sticky_prefix_hint.is_some() || preferred_user_id.is_some();
let exact_user_id = exact_user.and_then(|user| snapshot.user_id_by_name(user));
let has_hint = sticky_ip_hint.is_some()
|| sticky_prefix_hint.is_some()
|| preferred_user_id.is_some()
|| exact_user_id.is_some();
let overload = auth_probe_saturation_is_throttled_in(shared, Instant::now());
let candidate_budget = budget_for_validation(snapshot.entries().len(), overload, has_hint);
@@ -145,6 +187,7 @@ where
&entry.secret,
config,
is_tls,
mode_policy,
) {
matched_user = entry.user.clone();
matched_user_id = Some($user_id);
@@ -159,20 +202,20 @@ where
}};
}
let mut matched = false;
if let Some(user_id) = sticky_ip_hint {
let mut matched = exact_user_id.is_some_and(|user_id| try_user_id!(user_id));
if exact_user.is_none() && let Some(user_id) = sticky_ip_hint {
matched = try_user_id!(user_id);
}
if !matched && let Some(user_id) = preferred_user_id {
if exact_user.is_none() && !matched && let Some(user_id) = preferred_user_id {
matched = try_user_id!(user_id);
}
if !matched && let Some(user_id) = sticky_prefix_hint {
if exact_user.is_none() && !matched && let Some(user_id) = sticky_prefix_hint {
matched = try_user_id!(user_id);
}
if !matched && !budget_exhausted {
if exact_user.is_none() && !matched && !budget_exhausted {
let ring = &shared.handshake.recent_user_ring;
if !ring.is_empty() {
let next_seq = shared
@@ -197,7 +240,7 @@ where
}
}
if !matched && !budget_exhausted {
if exact_user.is_none() && !matched && !budget_exhausted {
for idx in 0..snapshot.entries().len() {
let Some(user_id) = u32::try_from(idx).ok() else {
break;
@@ -317,7 +360,16 @@ where
success,
));
} else {
let decoded_users = decode_user_secrets_in(shared, config, preferred_user);
let decoded_users = match exact_user {
Some(user) => config
.access
.users
.get(user)
.and_then(|secret| decode_user_secret(shared, user, secret))
.map(|secret| vec![(user.to_string(), secret)])
.unwrap_or_default(),
None => decode_user_secrets_in(shared, config, preferred_user),
};
let mut validation_checks = 0usize;
for (user, secret) in decoded_users {
@@ -337,6 +389,7 @@ where
&secret_arr,
config,
is_tls,
mode_policy,
) else {
continue;
};