Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions crates/api/src/node_admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ fn check_plugins(cfg: &Value) -> Result<()> {
}
Ok(())
}
let cfg = object(cfg, &["ingressFilter", "egressFilter", "torrentBlocker", "sharedLists"])?;
let cfg = object(cfg, &["ingressFilter", "egressFilter", "torrentBlocker", "sharedLists", "antiScanner"])?;
let mut names = Vec::new();
if let Some(lists) = cfg.get("sharedLists") {
for list in lists.as_array().ok_or_else(|| Error::bad("общие списки должны быть массивом"))? {
Expand Down Expand Up @@ -592,6 +592,30 @@ fn check_plugins(cfg: &Value) -> Result<()> {
return Err(Error::bad("срок блокировки — целое число от 0 до 604800 секунд"));
}
}
if let Some(val) = cfg.get("antiScanner") {
let map = object(val, &["enabled", "sources", "updateIntervalSecs", "customIps"])?;
if map.get("enabled").is_some_and(|v| !v.is_boolean()) {
return Err(Error::bad("enabled должен быть true или false"));
}
if let Some(sources) = map.get("sources") {
let arr = sources.as_array().ok_or_else(|| Error::bad("sources должны быть массивом"))?;
for s in arr {
let url = s.as_str().ok_or_else(|| Error::bad("URL источника должен быть строкой"))?;
if !url.starts_with("http://") && !url.starts_with("https://") {
return Err(Error::bad("источник антисканера должен быть URL (http:// или https://)"));
}
}
}
if let Some(interval) = map.get("updateIntervalSecs") {
let n = interval.as_u64().ok_or_else(|| Error::bad("updateIntervalSecs должен быть целым числом секунд"))?;
if !(300..=604_800).contains(&n) {
return Err(Error::bad("интервал обновления — от 300 до 604800 секунд (от 5 мин до 7 дней)"));
}
}
if let Some(ips) = map.get("customIps") {
addresses(ips, &names, true)?;
}
}
Ok(())
}

Expand All @@ -601,11 +625,24 @@ mod plugin_validation_tests {
#[test]
fn accepts_filters_and_resolved_shared_lists() {
assert!(check_plugins(&json!({"ingressFilter":{"enabled":true,"blockedIps":["ext:office"]},"sharedLists":[{"name":"office","items":["192.0.2.0/24","2001:db8::/32"]}]})).is_ok());
assert!(check_plugins(&json!({"antiScanner":{"enabled":true,"sources":["https://example.com/anti.list"],"updateIntervalSecs":3600,"customIps":["198.51.100.0/24"]}})).is_ok());
assert!(check_plugins(&json!({})).is_ok());
}
#[test]
fn rejects_ignored_fields_invalid_masks_and_types() {
for cfg in [json!({"ip_filter":{}}),json!({"egressFilter":{"enabled":"true"}}),json!({"ingressFilter":{"blockedIps":["192.0.2.1/99"]}}),json!({"egressFilter":{"blockedPorts":[0]}}),json!({"torrentBlocker":{"blockDuration":-1}}),json!({"ingressFilter":{"blockedIps":["ext:missing"]}}),json!({"sharedLists":[{"name":"a","items":["ext:a"]}]})] {
for cfg in [
json!({"ip_filter":{}}),
json!({"egressFilter":{"enabled":"true"}}),
json!({"ingressFilter":{"blockedIps":["192.0.2.1/99"]}}),
json!({"egressFilter":{"blockedPorts":[0]}}),
json!({"torrentBlocker":{"blockDuration":-1}}),
json!({"ingressFilter":{"blockedIps":["ext:missing"]}}),
json!({"sharedLists":[{"name":"a","items":["ext:a"]}]}),
json!({"antiScanner":{"enabled":"true"}}),
json!({"antiScanner":{"updateIntervalSecs":60}}),
json!({"antiScanner":{"sources":["ftp://example.com"]}}),
json!({"antiScanner":{"customIps":["999.999.999.999"]}}),
] {
assert!(check_plugins(&cfg).is_err(), "accepted {cfg}");
}
}
Expand Down
33 changes: 33 additions & 0 deletions crates/node/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ async fn main() {
agent_token: None,
torrent_block: None,
selfsteal: selfsteal::Controller::default(),
antiscanner_last_sync: None,
antiscanner_enabled: false,
};

let mut sync_tick = tokio::time::interval(std::time::Duration::from_secs(settings.sync_interval));
Expand Down Expand Up @@ -277,6 +279,8 @@ struct NodeState {
/// ноды: иначе владелец не смог бы выключить сбор, не заходя на
/// каждый сервер.
want_domains: bool,
antiscanner_last_sync: Option<std::time::Instant>,
antiscanner_enabled: bool,
}

/// Откуда агент берёт свою новую сборку.
Expand Down Expand Up @@ -355,6 +359,7 @@ async fn sync(
plugins_status: Some({
let mut st = plugins::probe();
st.applied = state.plugins_applied.is_some();
st.antiscanner_enabled = state.antiscanner_enabled;
let mut value = json!(st);
value["selfsteal"] = json!(state.selfsteal.status);
value
Expand Down Expand Up @@ -399,6 +404,34 @@ async fn sync(
// Плагины применяем при каждом изменении, не дожидаясь смены
// конфига движка: блокировку адреса ждать 15 минут нельзя.
if let Some(cfg) = &sync.plugins {
if cfg.anti_scanner.enabled {
state.antiscanner_enabled = true;
let interval = std::time::Duration::from_secs(cfg.anti_scanner.update_interval_secs.max(300));
let needs_sync = match state.antiscanner_last_sync {
None => plugins::load_antiscanner_ips().is_empty(),
Some(last) => last.elapsed() >= interval,
};
if needs_sync {
let sources = if cfg.anti_scanner.sources.is_empty() {
plugins::DEFAULT_ANTISCANNER_SOURCES.iter().map(|s| s.to_string()).collect()
} else {
cfg.anti_scanner.sources.clone()
};
match plugins::sync_antiscanner_lists(http, &sources).await {
Ok(count) => {
state.antiscanner_last_sync = Some(std::time::Instant::now());
tracing::info!(count, "списки антисканера обновлены");
state.plugins_applied = None;
}
Err(e) => {
tracing::warn!(error = %e, "не удалось обновить списки антисканера");
}
}
}
} else {
state.antiscanner_enabled = false;
}

let fingerprint = format!("{cfg:?}");
// Ничего не включено — не трогаем nftables вовсе. Иначе на ноде
// без него агент писал предупреждение каждые пятнадцать секунд
Expand Down
Loading