mirror of
https://github.com/telemt/telemt.git
synced 2026-04-18 02:54:10 +03:00
Gray Action for API by #630
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
#![allow(clippy::too_many_arguments)]
|
#![allow(clippy::too_many_arguments)]
|
||||||
|
|
||||||
use std::convert::Infallible;
|
use std::io::{Error as IoError, ErrorKind};
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -16,7 +16,7 @@ use tokio::net::TcpListener;
|
|||||||
use tokio::sync::{Mutex, RwLock, watch};
|
use tokio::sync::{Mutex, RwLock, watch};
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
use crate::config::ProxyConfig;
|
use crate::config::{ApiGrayAction, ProxyConfig};
|
||||||
use crate::ip_tracker::UserIpTracker;
|
use crate::ip_tracker::UserIpTracker;
|
||||||
use crate::proxy::route_mode::RouteRuntimeController;
|
use crate::proxy::route_mode::RouteRuntimeController;
|
||||||
use crate::startup::StartupTracker;
|
use crate::startup::StartupTracker;
|
||||||
@@ -184,8 +184,10 @@ pub async fn serve(
|
|||||||
.serve_connection(hyper_util::rt::TokioIo::new(stream), svc)
|
.serve_connection(hyper_util::rt::TokioIo::new(stream), svc)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
if !error.is_user() {
|
||||||
debug!(error = %error, "API connection error");
|
debug!(error = %error, "API connection error");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,7 +197,7 @@ async fn handle(
|
|||||||
peer: SocketAddr,
|
peer: SocketAddr,
|
||||||
shared: Arc<ApiShared>,
|
shared: Arc<ApiShared>,
|
||||||
config_rx: watch::Receiver<Arc<ProxyConfig>>,
|
config_rx: watch::Receiver<Arc<ProxyConfig>>,
|
||||||
) -> Result<Response<Full<Bytes>>, Infallible> {
|
) -> Result<Response<Full<Bytes>>, IoError> {
|
||||||
let request_id = shared.next_request_id();
|
let request_id = shared.next_request_id();
|
||||||
let cfg = config_rx.borrow().clone();
|
let cfg = config_rx.borrow().clone();
|
||||||
let api_cfg = &cfg.server.api;
|
let api_cfg = &cfg.server.api;
|
||||||
@@ -213,14 +215,27 @@ async fn handle(
|
|||||||
|
|
||||||
if !api_cfg.whitelist.is_empty() && !api_cfg.whitelist.iter().any(|net| net.contains(peer.ip()))
|
if !api_cfg.whitelist.is_empty() && !api_cfg.whitelist.iter().any(|net| net.contains(peer.ip()))
|
||||||
{
|
{
|
||||||
return Ok(error_response(
|
return match api_cfg.gray_action {
|
||||||
|
ApiGrayAction::Api => Ok(error_response(
|
||||||
request_id,
|
request_id,
|
||||||
ApiFailure::new(
|
ApiFailure::new(
|
||||||
StatusCode::FORBIDDEN,
|
StatusCode::FORBIDDEN,
|
||||||
"forbidden",
|
"forbidden",
|
||||||
"Source IP is not allowed",
|
"Source IP is not allowed",
|
||||||
),
|
),
|
||||||
));
|
)),
|
||||||
|
ApiGrayAction::Ok200 => Ok(
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header("content-type", "text/html; charset=utf-8")
|
||||||
|
.body(Full::new(Bytes::new()))
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
ApiGrayAction::Drop => Err(IoError::new(
|
||||||
|
ErrorKind::ConnectionAborted,
|
||||||
|
"api request dropped by gray_action=drop",
|
||||||
|
)),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if !api_cfg.auth_header.is_empty() {
|
if !api_cfg.auth_header.is_empty() {
|
||||||
|
|||||||
@@ -560,6 +560,7 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|
|||||||
if old.server.api.enabled != new.server.api.enabled
|
if old.server.api.enabled != new.server.api.enabled
|
||||||
|| old.server.api.listen != new.server.api.listen
|
|| old.server.api.listen != new.server.api.listen
|
||||||
|| old.server.api.whitelist != new.server.api.whitelist
|
|| old.server.api.whitelist != new.server.api.whitelist
|
||||||
|
|| old.server.api.gray_action != new.server.api.gray_action
|
||||||
|| old.server.api.auth_header != new.server.api.auth_header
|
|| old.server.api.auth_header != new.server.api.auth_header
|
||||||
|| old.server.api.request_body_limit_bytes != new.server.api.request_body_limit_bytes
|
|| old.server.api.request_body_limit_bytes != new.server.api.request_body_limit_bytes
|
||||||
|| old.server.api.minimal_runtime_enabled != new.server.api.minimal_runtime_enabled
|
|| old.server.api.minimal_runtime_enabled != new.server.api.minimal_runtime_enabled
|
||||||
|
|||||||
@@ -1492,6 +1492,7 @@ mod tests {
|
|||||||
assert_eq!(cfg.censorship.unknown_sni_action, UnknownSniAction::Drop);
|
assert_eq!(cfg.censorship.unknown_sni_action, UnknownSniAction::Drop);
|
||||||
assert_eq!(cfg.server.api.listen, default_api_listen());
|
assert_eq!(cfg.server.api.listen, default_api_listen());
|
||||||
assert_eq!(cfg.server.api.whitelist, default_api_whitelist());
|
assert_eq!(cfg.server.api.whitelist, default_api_whitelist());
|
||||||
|
assert_eq!(cfg.server.api.gray_action, ApiGrayAction::Drop);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cfg.server.api.request_body_limit_bytes,
|
cfg.server.api.request_body_limit_bytes,
|
||||||
default_api_request_body_limit_bytes()
|
default_api_request_body_limit_bytes()
|
||||||
@@ -1662,6 +1663,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(server.api.listen, default_api_listen());
|
assert_eq!(server.api.listen, default_api_listen());
|
||||||
assert_eq!(server.api.whitelist, default_api_whitelist());
|
assert_eq!(server.api.whitelist, default_api_whitelist());
|
||||||
|
assert_eq!(server.api.gray_action, ApiGrayAction::Drop);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
server.api.request_body_limit_bytes,
|
server.api.request_body_limit_bytes,
|
||||||
default_api_request_body_limit_bytes()
|
default_api_request_body_limit_bytes()
|
||||||
@@ -1809,6 +1811,59 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn api_gray_action_parses_and_defaults_to_drop() {
|
||||||
|
let cfg_default: ProxyConfig = toml::from_str(
|
||||||
|
r#"
|
||||||
|
[server]
|
||||||
|
[general]
|
||||||
|
[network]
|
||||||
|
[access]
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(cfg_default.server.api.gray_action, ApiGrayAction::Drop);
|
||||||
|
|
||||||
|
let cfg_api: ProxyConfig = toml::from_str(
|
||||||
|
r#"
|
||||||
|
[server]
|
||||||
|
[general]
|
||||||
|
[network]
|
||||||
|
[access]
|
||||||
|
[server.api]
|
||||||
|
gray_action = "api"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(cfg_api.server.api.gray_action, ApiGrayAction::Api);
|
||||||
|
|
||||||
|
let cfg_200: ProxyConfig = toml::from_str(
|
||||||
|
r#"
|
||||||
|
[server]
|
||||||
|
[general]
|
||||||
|
[network]
|
||||||
|
[access]
|
||||||
|
[server.api]
|
||||||
|
gray_action = "200"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(cfg_200.server.api.gray_action, ApiGrayAction::Ok200);
|
||||||
|
|
||||||
|
let cfg_drop: ProxyConfig = toml::from_str(
|
||||||
|
r#"
|
||||||
|
[server]
|
||||||
|
[general]
|
||||||
|
[network]
|
||||||
|
[access]
|
||||||
|
[server.api]
|
||||||
|
gray_action = "drop"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(cfg_drop.server.api.gray_action, ApiGrayAction::Drop);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dc_overrides_allow_string_and_array() {
|
fn dc_overrides_allow_string_and_array() {
|
||||||
let toml = r#"
|
let toml = r#"
|
||||||
|
|||||||
@@ -1183,6 +1183,13 @@ pub struct ApiConfig {
|
|||||||
#[serde(default = "default_api_whitelist")]
|
#[serde(default = "default_api_whitelist")]
|
||||||
pub whitelist: Vec<IpNetwork>,
|
pub whitelist: Vec<IpNetwork>,
|
||||||
|
|
||||||
|
/// Behavior for requests from source IPs outside `whitelist`.
|
||||||
|
/// - `api`: return structured API forbidden response.
|
||||||
|
/// - `200`: return `200 OK` with an empty body.
|
||||||
|
/// - `drop`: close the connection without HTTP response.
|
||||||
|
#[serde(default)]
|
||||||
|
pub gray_action: ApiGrayAction,
|
||||||
|
|
||||||
/// Optional static value for `Authorization` header validation.
|
/// Optional static value for `Authorization` header validation.
|
||||||
/// Empty string disables header auth.
|
/// Empty string disables header auth.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -1227,6 +1234,7 @@ impl Default for ApiConfig {
|
|||||||
enabled: default_true(),
|
enabled: default_true(),
|
||||||
listen: default_api_listen(),
|
listen: default_api_listen(),
|
||||||
whitelist: default_api_whitelist(),
|
whitelist: default_api_whitelist(),
|
||||||
|
gray_action: ApiGrayAction::default(),
|
||||||
auth_header: String::new(),
|
auth_header: String::new(),
|
||||||
request_body_limit_bytes: default_api_request_body_limit_bytes(),
|
request_body_limit_bytes: default_api_request_body_limit_bytes(),
|
||||||
minimal_runtime_enabled: default_api_minimal_runtime_enabled(),
|
minimal_runtime_enabled: default_api_minimal_runtime_enabled(),
|
||||||
@@ -1240,6 +1248,19 @@ impl Default for ApiConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ApiGrayAction {
|
||||||
|
/// Preserve current API behavior for denied source IPs.
|
||||||
|
Api,
|
||||||
|
/// Mimic a plain web endpoint by returning `200 OK` with an empty body.
|
||||||
|
#[serde(rename = "200")]
|
||||||
|
Ok200,
|
||||||
|
/// Drop connection without HTTP response for denied source IPs.
|
||||||
|
#[default]
|
||||||
|
Drop,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum ConntrackMode {
|
pub enum ConntrackMode {
|
||||||
|
|||||||
Reference in New Issue
Block a user