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.