version 2
#!/usr/bin/env python3
"""
Скрипт параллельной проверки доступности сетевых узлов и TCP-портов.
Выполняет ICMP Ping (для хостов без портов) или TCP Connect (для хостов с портами).
Выводит результат в формате CSV в stdout (удобно для экспорта в Excel).
"""
import asyncio
import csv
import sys
import argparse
import time
# ==============================================================================
# КОНФИГУРАЦИЯ И НАСТРОЙКИ
# ==============================================================================
# Таймаут ожидания ответа от хоста/порта (в секундах)
# Если за это время отклик не получен, ресурс считается недоступным.
TIMEOUT = 3.0
# Значение задержки (Latency), которое записывается при недоступности
UNREACHABLE_VAL = 999
# Количество повторных попыток (retries) перед тем, как признать ресурс недоступным (999)
# 1 = только одна попытка, 2 = две попытки (первичная + 1 повтор) и т.д.
MAX_RETRIES = 2
# Задержка между повторными попытками (в секундах)
RETRY_DELAY = 0.5
# Ограничение количества ОДНОВРЕМЕННЫХ проверок (Семафор)
# Предотвращает исчерпание сокетов/дескрипторов на клиенте и исключает
# непреднамеренную DoS-нагрузку (и срабатывание IDS/Fail2ban) на целевые узлы.
MAX_CONCURRENT_TASKS = 50
# Файл со списком хостов по умолчанию
DEFAULT_HOSTS_FILE = 'hosts.lst'
# ==============================================================================
# Глобальный семафор для ограничения параллелизма
semaphore = asyncio.Semaphore(MAX_CONCURRENT_TASKS)
async def ping_icmp(host: str) -> float:
"""
Проверка доступности хоста с помощью ICMP Ping (системная утилита).
Возвращает время отклика в миллисекундах или UNREACHABLE_VAL при ошибке.
"""
async with semaphore:
for attempt in range(1, MAX_RETRIES + 1):
start_time = time.perf_counter()
try:
proc = await asyncio.create_subprocess_exec(
'ping', '-c', '1', '-W', str(int(TIMEOUT)), host,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL
)
await asyncio.wait_for(proc.wait(), timeout=TIMEOUT + 0.5)
elapsed = (time.perf_counter() - start_time) * 1000
if proc.returncode == 0:
return round(elapsed, 2)
except (asyncio.TimeoutError, Exception):
pass
# Задержка перед повторной попыткой
if attempt < MAX_RETRIES:
await asyncio.sleep(RETRY_DELAY)
return UNREACHABLE_VAL
async def check_tcp(host: str, port: int) -> float:
"""
Проверка доступности конкретного TCP-порта.
Корректно открывает и сразу закрывает соединение (FIN/RST),
чтобы не оставлять зависших сокетов на сервере.
"""
async with semaphore:
for attempt in range(1, MAX_RETRIES + 1):
start_time = time.perf_counter()
try:
conn = asyncio.open_connection(host, port)
reader, writer = await asyncio.wait_for(conn, timeout=TIMEOUT)
elapsed = (time.perf_counter() - start_time) * 1000
# Корректное закрытие TCP-сессии
writer.close()
await writer.wait_closed()
return round(elapsed, 2)
except (asyncio.TimeoutError, OSError, Exception):
pass
# Задержка перед повторной попыткой
if attempt < MAX_RETRIES:
await asyncio.sleep(RETRY_DELAY)
return UNREACHABLE_VAL
async def check_target(target_str: str) -> list:
"""
Разбор одной строки из файла hosts.lst и запуск соответствующей проверки.
Игнорирует пустые строки и комментарии (#).
"""
line = target_str.strip()
if not line or line.startswith('#'):
return None
parts = line.split()
host = parts[0]
ports = []
# Если в строке указаны порты через запятую (например: meta.ua 80,443,22)
if len(parts) > 1:
ports_str = parts[1]
for p in ports_str.split(','):
p_clean = p.strip()
if p_clean.isdigit():
ports.append(int(p_clean))
results = []
if not ports:
# Проверка по ICMP Ping
latency = await ping_icmp(host)
results.append({
'Host': host,
'Type': 'ICMP',
'Port': '-',
'Latency_ms': latency
})
else:
# Параллельная проверка всех указанных TCP-портов для хоста
tasks = [check_tcp(host, port) for port in ports]
latencies = await asyncio.gather(*tasks)
for port, latency in zip(ports, latencies):
results.append({
'Host': host,
'Type': 'TCP',
'Port': port,
'Latency_ms': latency
})
return results
async def main():
parser = argparse.ArgumentParser(
description="Параллельная проверка сетевой доступности хостов и TCP-портов с выводом в CSV."
)
parser.add_argument(
'file',
nargs='?',
default=DEFAULT_HOSTS_FILE,
help=f'Путь к файлу со списком хостов (по умолчанию: {DEFAULT_HOSTS_FILE})'
)
args = parser.parse_args()
# Чтение списка хостов
try:
with open(args.file, 'r', encoding='utf-8') as f:
lines = f.readlines()
except FileNotFoundError:
sys.stderr.write(f"Ошибка: Файл '{args.file}' не найден.\n")
sys.exit(1)
# Запуск параллельной проверки всех записей
tasks = [check_target(line) for line in lines]
raw_results = await asyncio.gather(*tasks)
# Вывод результатов в CSV на stdout
writer = csv.writer(sys.stdout)
writer.writerow(['Host', 'Type', 'Port', 'Latency_ms'])
for item in raw_results:
if item:
for row in item:
writer.writerow([row['Host'], row['Type'], row['Port'], row['Latency_ms']])
if __name__ == '__main__':
asyncio.run(main())
---
Подробное описание логики и реализованных архитектурных решений
1. Асинхронность и высокий параллелизм (asyncio)
- Вместо медленной последовательной проверки или ресурсоемкого создания множества системных потоков (
threading), код использует корутины и событийный циклasyncio. - Несколько сот хостов и портов опрашиваются параллельно за пару секунд.
2. Защита сети и клиента — Семафор (MAX_CONCURRENT_TASKS)
asyncio.Semaphore(50)ставит лимит на количество одновременных сетевых соединений.- Зачем нужно: Если в списке будет 2000 хостов, без семафора ОС попытается сразу открыть 2000 сокетов, что может привезти к ошибке
Too many open filesна Linux, а также может быть расценено защитными системами (IDS / Fail2ban) как шторм или сканирование портов.
3. Логика повторных попыток (MAX_RETRIES и RETRY_DELAY)
- Сетевые пакеты могут теряться из-за кратковременных всплесков загрузки сети (jitter/packet loss).
- Если первая попытка отваливается по таймауту, скрипт делает паузу (
RETRY_DELAY = 0.5сек) и пробует еще раз. Если и повтор неудачен — только тогда ставится метка999. Это исключает ложные срабатывания.
4. Корректность закрытия соединений (Чистота TCP)
- В функции
check_tcpиспользуются командыwriter.close()иawait writer.wait_closed(). - Это гарантирует, что на удаленном сервере не останется «повисших» полуоткрытых соединений (CLOSE_WAIT/FIN_WAIT), а серверное приложение сразу поймет, что клиент завершил сессию.
5. Гибкая конфигурация и вывод
- Все критические параметры (
TIMEOUT,UNREACHABLE_VAL,MAX_RETRIES,MAX_CONCURRENT_TASKS) вынесены в начало скрипта для быстрой настройки под задачи компании или специфику сети. - Вывод строго соответствует формату CSV, готовому для перенаправления в файл (
./netcheck.py > report.csv) и открытия в MS Excel или LibreOffice Calc.