mirror of
https://github.com/telemt/telemt.git
synced 2026-05-13 23:31:44 +03:00
Merge pull request #770 from konstpic/feat/user-source-deny-list
feat(access): add per-user source IP deny list checks
This commit is contained in:
@@ -178,6 +178,21 @@ Notes:
|
|||||||
| `data_quota_bytes` | `u64` | no | Per-user traffic quota. |
|
| `data_quota_bytes` | `u64` | no | Per-user traffic quota. |
|
||||||
| `max_unique_ips` | `usize` | no | Per-user unique source IP limit. |
|
| `max_unique_ips` | `usize` | no | Per-user unique source IP limit. |
|
||||||
|
|
||||||
|
### `access.user_source_deny` via API
|
||||||
|
- In current API surface, per-user deny-list is **not** exposed as a dedicated field in `CreateUserRequest` / `PatchUserRequest`.
|
||||||
|
- Configure it in `config.toml` under `[access.user_source_deny]` and apply via normal config reload path.
|
||||||
|
- Runtime behavior after apply:
|
||||||
|
- auth succeeds for username/secret
|
||||||
|
- source IP is checked against `access.user_source_deny[username]`
|
||||||
|
- on match, handshake is rejected with the same fail-closed outcome as invalid auth
|
||||||
|
|
||||||
|
Example config:
|
||||||
|
```toml
|
||||||
|
[access.user_source_deny]
|
||||||
|
alice = ["203.0.113.0/24", "2001:db8:abcd::/48"]
|
||||||
|
bob = ["198.51.100.42/32"]
|
||||||
|
```
|
||||||
|
|
||||||
### `RotateSecretRequest`
|
### `RotateSecretRequest`
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
|
|||||||
@@ -2886,6 +2886,7 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
|
|||||||
| [`user_max_unique_ips_global_each`](#user_max_unique_ips_global_each) | `usize` | `0` |
|
| [`user_max_unique_ips_global_each`](#user_max_unique_ips_global_each) | `usize` | `0` |
|
||||||
| [`user_max_unique_ips_mode`](#user_max_unique_ips_mode) | `"active_window"`, `"time_window"`, or `"combined"` | `"active_window"` |
|
| [`user_max_unique_ips_mode`](#user_max_unique_ips_mode) | `"active_window"`, `"time_window"`, or `"combined"` | `"active_window"` |
|
||||||
| [`user_max_unique_ips_window_secs`](#user_max_unique_ips_window_secs) | `u64` | `30` |
|
| [`user_max_unique_ips_window_secs`](#user_max_unique_ips_window_secs) | `u64` | `30` |
|
||||||
|
| [`user_source_deny`](#user_source_deny) | `Map<String, IpNetwork[]>` | `{}` |
|
||||||
| [`replay_check_len`](#replay_check_len) | `usize` | `65536` |
|
| [`replay_check_len`](#replay_check_len) | `usize` | `65536` |
|
||||||
| [`replay_window_secs`](#replay_window_secs) | `u64` | `120` |
|
| [`replay_window_secs`](#replay_window_secs) | `u64` | `120` |
|
||||||
| [`ignore_time_skew`](#ignore_time_skew) | `bool` | `false` |
|
| [`ignore_time_skew`](#ignore_time_skew) | `bool` | `false` |
|
||||||
@@ -2990,6 +2991,20 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
|
|||||||
[access]
|
[access]
|
||||||
user_max_unique_ips_window_secs = 30
|
user_max_unique_ips_window_secs = 30
|
||||||
```
|
```
|
||||||
|
## user_source_deny
|
||||||
|
- **Constraints / validation**: Table `username -> IpNetwork[]`. Each network must parse as CIDR (for example `203.0.113.0/24` or `2001:db8::/32`).
|
||||||
|
- **Description**: Per-user source IP/CIDR deny-list applied **after successful auth** in TLS and MTProto handshake paths. A matched source IP is rejected via the same fail-closed path as invalid auth.
|
||||||
|
- **Example**:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[access.user_source_deny]
|
||||||
|
alice = ["203.0.113.0/24", "2001:db8:abcd::/48"]
|
||||||
|
bob = ["198.51.100.42/32"]
|
||||||
|
```
|
||||||
|
|
||||||
|
- **How it works (quick check)**:
|
||||||
|
- connection from user `alice` and source `203.0.113.55` -> rejected (matches `203.0.113.0/24`)
|
||||||
|
- connection from user `alice` and source `198.51.100.10` -> allowed by this rule set (no match)
|
||||||
## replay_check_len
|
## replay_check_len
|
||||||
- **Constraints / validation**: `usize`.
|
- **Constraints / validation**: `usize`.
|
||||||
- **Description**: Replay-protection storage length (number of entries tracked for duplicate detection).
|
- **Description**: Replay-protection storage length (number of entries tracked for duplicate detection).
|
||||||
|
|||||||
@@ -1893,6 +1893,12 @@ pub struct AccessConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub cidr_rate_limits: HashMap<IpNetwork, RateLimitBps>,
|
pub cidr_rate_limits: HashMap<IpNetwork, RateLimitBps>,
|
||||||
|
|
||||||
|
/// Per-username client source IP/CIDR deny list. Checked after successful
|
||||||
|
/// authentication; matching IPs get the same rejection path as invalid auth
|
||||||
|
/// (handshake fails closed for that connection).
|
||||||
|
#[serde(default)]
|
||||||
|
pub user_source_deny: HashMap<String, Vec<IpNetwork>>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub user_max_unique_ips: HashMap<String, usize>,
|
pub user_max_unique_ips: HashMap<String, usize>,
|
||||||
|
|
||||||
@@ -1928,6 +1934,7 @@ impl Default for AccessConfig {
|
|||||||
user_data_quota: HashMap::new(),
|
user_data_quota: HashMap::new(),
|
||||||
user_rate_limits: HashMap::new(),
|
user_rate_limits: HashMap::new(),
|
||||||
cidr_rate_limits: HashMap::new(),
|
cidr_rate_limits: HashMap::new(),
|
||||||
|
user_source_deny: HashMap::new(),
|
||||||
user_max_unique_ips: HashMap::new(),
|
user_max_unique_ips: HashMap::new(),
|
||||||
user_max_unique_ips_global_each: default_user_max_unique_ips_global_each(),
|
user_max_unique_ips_global_each: default_user_max_unique_ips_global_each(),
|
||||||
user_max_unique_ips_mode: UserMaxUniqueIpsMode::default(),
|
user_max_unique_ips_mode: UserMaxUniqueIpsMode::default(),
|
||||||
@@ -1939,6 +1946,15 @@ impl Default for AccessConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AccessConfig {
|
||||||
|
/// Returns true if `ip` is contained in any CIDR listed for `username` under `user_source_deny`.
|
||||||
|
pub fn is_user_source_ip_denied(&self, username: &str, ip: IpAddr) -> bool {
|
||||||
|
self.user_source_deny
|
||||||
|
.get(username)
|
||||||
|
.is_some_and(|nets| nets.iter().any(|n| n.contains(ip)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct RateLimitBps {
|
pub struct RateLimitBps {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -1450,6 +1450,20 @@ where
|
|||||||
validated_secret.copy_from_slice(secret);
|
validated_secret.copy_from_slice(secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config
|
||||||
|
.access
|
||||||
|
.is_user_source_ip_denied(validated_user.as_str(), peer.ip())
|
||||||
|
{
|
||||||
|
auth_probe_record_failure_in(shared, peer.ip(), Instant::now());
|
||||||
|
maybe_apply_server_hello_delay(config).await;
|
||||||
|
warn!(
|
||||||
|
peer = %peer,
|
||||||
|
user = %validated_user,
|
||||||
|
"TLS handshake rejected: client source IP on per-user deny list (access.user_source_deny)"
|
||||||
|
);
|
||||||
|
return HandshakeResult::BadClient { reader, writer };
|
||||||
|
}
|
||||||
|
|
||||||
// Reject known replay digests before expensive cache/domain/ALPN policy work.
|
// Reject known replay digests before expensive cache/domain/ALPN policy work.
|
||||||
let digest_half = &validation_digest[..tls::TLS_DIGEST_HALF_LEN];
|
let digest_half = &validation_digest[..tls::TLS_DIGEST_HALF_LEN];
|
||||||
if replay_checker.check_tls_digest(digest_half) {
|
if replay_checker.check_tls_digest(digest_half) {
|
||||||
@@ -1795,6 +1809,20 @@ where
|
|||||||
|
|
||||||
let validation = matched_validation.expect("validation must exist when matched");
|
let validation = matched_validation.expect("validation must exist when matched");
|
||||||
|
|
||||||
|
if config
|
||||||
|
.access
|
||||||
|
.is_user_source_ip_denied(matched_user.as_str(), peer.ip())
|
||||||
|
{
|
||||||
|
auth_probe_record_failure_in(shared, peer.ip(), Instant::now());
|
||||||
|
maybe_apply_server_hello_delay(config).await;
|
||||||
|
warn!(
|
||||||
|
peer = %peer,
|
||||||
|
user = %matched_user,
|
||||||
|
"MTProto handshake rejected: client source IP on per-user deny list (access.user_source_deny)"
|
||||||
|
);
|
||||||
|
return HandshakeResult::BadClient { reader, writer };
|
||||||
|
}
|
||||||
|
|
||||||
// Apply replay tracking only after successful authentication.
|
// Apply replay tracking only after successful authentication.
|
||||||
//
|
//
|
||||||
// This ordering prevents an attacker from producing invalid handshakes that
|
// This ordering prevents an attacker from producing invalid handshakes that
|
||||||
@@ -1873,6 +1901,17 @@ where
|
|||||||
.auth_expensive_checks_total
|
.auth_expensive_checks_total
|
||||||
.fetch_add(validation_checks as u64, Ordering::Relaxed);
|
.fetch_add(validation_checks as u64, Ordering::Relaxed);
|
||||||
|
|
||||||
|
if config.access.is_user_source_ip_denied(user.as_str(), peer.ip()) {
|
||||||
|
auth_probe_record_failure_in(shared, peer.ip(), Instant::now());
|
||||||
|
maybe_apply_server_hello_delay(config).await;
|
||||||
|
warn!(
|
||||||
|
peer = %peer,
|
||||||
|
user = %user,
|
||||||
|
"MTProto handshake rejected: client source IP on per-user deny list (access.user_source_deny)"
|
||||||
|
);
|
||||||
|
return HandshakeResult::BadClient { reader, writer };
|
||||||
|
}
|
||||||
|
|
||||||
// Apply replay tracking only after successful authentication.
|
// Apply replay tracking only after successful authentication.
|
||||||
//
|
//
|
||||||
// This ordering prevents an attacker from producing invalid handshakes that
|
// This ordering prevents an attacker from producing invalid handshakes that
|
||||||
|
|||||||
Reference in New Issue
Block a user