惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

K
Kaspersky official blog
P
Privacy International News Feed
Simon Willison's Weblog
Simon Willison's Weblog
V
Vulnerabilities – Threatpost
Know Your Adversary
Know Your Adversary
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
P
Palo Alto Networks Blog
NISL@THU
NISL@THU
C
Cybersecurity and Infrastructure Security Agency CISA
S
Securelist
Scott Helme
Scott Helme
T
Threat Research - Cisco Blogs
L
LINUX DO - 热门话题
Google Online Security Blog
Google Online Security Blog
G
GRAHAM CLULEY
Project Zero
Project Zero
P
Privacy & Cybersecurity Law Blog
I
Intezer
T
Threatpost
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Y
Y Combinator Blog
大猫的无限游戏
大猫的无限游戏
S
Schneier on Security
WordPress大学
WordPress大学
P
Proofpoint News Feed
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
博客园 - Franky
小众软件
小众软件
S
Security Affairs
人人都是产品经理
人人都是产品经理
量子位
Help Net Security
Help Net Security
博客园 - 三生石上(FineUI控件)
V
Visual Studio Blog
PCI Perspectives
PCI Perspectives
雷峰网
雷峰网
A
Arctic Wolf
Apple Machine Learning Research
Apple Machine Learning Research
罗磊的独立博客
博客园 - 聂微东
H
Hacker News: Front Page
Jina AI
Jina AI
博客园 - 叶小钗
C
CXSECURITY Database RSS Feed - CXSecurity.com
L
LINUX DO - 最新话题
Latest news
Latest news
The Last Watchdog
The Last Watchdog
W
WeLiveSecurity
酷 壳 – CoolShell
酷 壳 – CoolShell

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Кто решает судьбу вашего проекта? Разбираем заинтересованные стороны. BABOK #1 Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как Фалькон Тех меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Пишем TCP-сканер портов на Go: goroutine, timeout и CSV-отчёт
Михаил · 2026-06-11 · via Все публикации подряд на Хабре

Средний

6 мин

2.7K

Недавно знакомый попросил помочь с небольшой задачей по проверке внешнего периметра сети компании. Сразу уточню: речь шла об инфраструктуре, на проверку которой было разрешение.

Под внешним периметром обычно понимают всё, что доступно из интернета: публичные IP-адреса, домены, поддомены, облачные или VPS-серверы, а также сервисы, которые слушают внешние порты.

Задача была простой по формулировке, но интересной технически: нужно понять, какие адреса доступны извне и к каким портам можно подключиться.

Что мы будем делать

В данной статье я покажу, как сделать простой TCP port scanner на Go.

Он будет уметь:

  • Читать IP-адреса и домены из файла

  • Проверять диапазон портов

  • Определять открытые порты и добавлять к ним условную оценку риска

  • Сразу реализуем ограничение параллельности через семафор, чтобы обработка портов была быстрее

Структура проекта и сам код

Проект небольшой, поэтому структура получилась простой. Я разделил код на несколько пакетов, чтобы каждая часть отвечала за свою задачу.

cmd/
  bin/
    main.go

internal/
  input/
    input.go
  report/
    csv.go
  resolver/
    resolver.go
  scanner/
    scanner.go
  services/
    services.go

perimeter.txt
go.mod
go.sum

Коротко пройдёмся по пакетам внутри internal и разберём, за что отвечает каждый из них. Input - отвечает за чтения файла и возвращения массива string с нашими портами:

func ReadTargets(path string) ([]string, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	return ParseTargets(file)
}

func ParseTargets(reader io.Reader) ([]string, error) {
	targets := make([]string, 0)

	scanner := bufio.NewScanner(reader)

	for scanner.Scan() {
		text := strings.TrimSpace(scanner.Text())
		if text == "" || strings.HasPrefix(text, "#") {
			continue
		}
		targets = append(targets, text)
	}

	if err := scanner.Err(); err != nil {
		return nil, err
	}

	return targets, nil
}

Здесь всё просто: открываем файл, читаем его построчно через bufio.Scanner, пропускаем пустые строки и комментарии, а остальные значения возвращаем как список целей.

Services - данный пакет отвечает за справочную информацию о сервисах по номеру порта:

package services

type Info struct {
	Name string
	Risk string
}

func Lookup(port int) Info {
	switch port {
	case 22:
		return Info{Name: "SSH", Risk: "High"}
	case 80:
		return Info{Name: "HTTP", Risk: "Medium"}
	case 443:
		return Info{Name: "HTTPS", Risk: "Low"}
	case 3306:
		return Info{Name: "MySQL", Risk: "High"}
	case 3389:
		return Info{Name: "RDP", Risk: "High"}
	case 5432:
		return Info{Name: "PostgreSQL", Risk: "High"}
	case 6379:
		return Info{Name: "Redis", Risk: "High"}
	default:
		return Info{Name: "Unknown", Risk: "Unknown"}
	}
}

Lookup не делает fingerprint сервиса. Он просто подсказывает наиболее вероятный сервис по номеру порта.

Структура Info - хранит в себе Name - это названия сервиса, например SSH или HTTP. А Risk - это условный уровень риска (Low, Medium, High, Unknown).

Функция Lookup - получает порт смотрит к какому сервису он относиться и возвращает нам нашу структуру. Тоже довольно просто.

Далее нам в пакете Scanner - надо описать структуру Result в которой как у нас будет вся нужная нам информация:

package scanner

type Result struct {
	Target       string
	IP           string
	Port         int
	Protocol     string
	ServiceGuess string
	Status       string
	Risk         string
	Error        string
}

Данная структура просто формат ответа: какой домен/IP проверяли, какой порт, открыт он или закрыт, какой сервис, какой риск, была ли ошибка.

Далее по списку нужно сделать функцию которая будем превращать домены в IP адреса и это функция будет лежать у нас в пакете resolver:

package resolver

import "net"

func ResolveTarget(target string) ([]string, error) {
	parsedIP := net.ParseIP(target)
	if parsedIP != nil {
		if parsedIP.To4() == nil {
			return nil, nil
		}
		return []string{parsedIP.String()}, nil
	}

	ips, err := net.LookupIP(target)
	if err != nil {
		return nil, err
	}

	targets := make([]string, 0)
	for _, ip := range ips {
		if ip.To4() != nil {
			targets = append(targets, ip.String())
		}
	}

	return targets, nil
}

Что здесь происходит, наша функция ResolveTarget принимает наши "Цели" - и смотрит является ли они IP адресами, если нет преобразует в IP адрес и возвращает.

ResolveTarget принимает строку из файла. Если это уже IPv4-адрес, функция сразу возвращает его. Если это домен, она делает DNS-lookup через net.LookupIP и возвращает найденные IPv4-адреса.

Теперь вернемся к нашему пакет Scanner - тут мы должны описать функцию ScanPort, сначала покажу а потом объясню:

func ScanPort(target string, ip string, port int, timeout time.Duration) Result {
	info := services.Lookup(port)

	result := Result{
		Target:       target,
		IP:           ip,
		Port:         port,
		Protocol:     "tcp",
		ServiceGuess: info.Name,
		Risk:         info.Risk,
	}

	address := net.JoinHostPort(ip, strconv.Itoa(port))

	conn, err := net.DialTimeout("tcp", address, timeout)
	if err != nil {
		if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
			result.Status = "filtered"
			result.Error = netErr.Error()
			return result
		}

		result.Status = "closed"
		result.Error = err.Error()
		return result
	}

	conn.Close()
	result.Status = "open"
	return result
}

Функция выглядит по сложней чем другие, но уверяю вас тут все легко.

ScanPort получает цель, IP, порт и timeout. Сначала мы получаем информацию о предполагаемом сервисе через services.Lookup. Затем собираем адрес через net.JoinHostPort — это безопаснее, чем склеивать ip + ":" + port вручную.

После этого вызываем net.DialTimeout. Если соединение удалось, считаем порт открытым. Если произошла ошибка, считаем порт закрытым. Если ошибка связана с timeout, помечаем статус как filtered.

Статус filtered здесь условный: я использую его для случаев, когда соединение не было явно отклонено, а завершилось по timeout.

Ну и если ошибки не было просто говорим что статус = открыто и возвращаем на результат.

Последний технический кусок — пакет report. Он отвечает за сохранение результатов в CSV-файл.

package report

import (
	"encoding/csv"
	"io"
	"os"
	"perimeter-audit/internal/scanner"
	"strconv"
)

func WriteCSV(results []scanner.Result, path string) error {
	file, err := os.Create(path)
	if err != nil {
		return err
	}
	defer file.Close()

	return WriteCSVWriter(file, results)
}

func WriteCSVWriter(writer io.Writer, results []scanner.Result) error {
	csvWriter := csv.NewWriter(writer)

	err := csvWriter.Write([]string{"target", "ip", "port", "protocol", "service_guess", "status", "risk", "error"})
	if err != nil {
		return err
	}

	for _, result := range results {
		if err := csvWriter.Write([]string{result.Target, result.IP, strconv.Itoa(result.Port), result.Protocol, result.ServiceGuess, result.Status, result.Risk, result.Error}); err != nil {
			return err
		}
	}

	csvWriter.Flush()

	if err := csvWriter.Error(); err != nil {
		return err
	}

	return nil
}

Тут у нас report сохраняет результаты сканирования в CSV-файл: создает файл, записывает заголовки колонок и добавляет по строке на каждый результат.

Теперь осталось связать все части в main.go: прочитать путь к файлу через флаг -input, загрузить цели, просканировать их и сохранить результат в CSV.

package main

var defaultPorts = []int{21, 22, 23, 25, 53, 80, 110, 143, 443, 445, 1433, 3306, 3389, 5432, 5900, 6379, 8080, 8443, 9200, 27017}

func main() {
	inputFlag := flag.String("input", "", "path to targets file")
	outputFlag := flag.String("output", "report.csv", "path to CSV report")
	flag.Parse()

	if *inputFlag == "" {
		fmt.Fprintln(os.Stderr, "input flag is required")
		os.Exit(1)
	}

	targets, err := input.ReadTargets(*inputFlag)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error reading targets: %v\n", err)
		os.Exit(1)
	}

	results := scanTargets(targets, defaultPorts, 2*time.Second, 50)
	if err := report.WriteCSV(results, *outputFlag); err != nil {
		fmt.Fprintf(os.Stderr, "Error writing report: %v\n", err)
		os.Exit(1)
	}

	fmt.Printf("Results written to %s\n", *outputFlag)
}

func scanTargets(targets []string, ports []int, timeout time.Duration, maxConcurrency int) []scanner.Result {
	resultCh := make(chan scanner.Result)
	var wg sync.WaitGroup
	sem := make(chan struct{}, maxConcurrency)
	results := make([]scanner.Result, 0)

	for _, target := range targets {
		ips, err := resolver.ResolveTarget(target)
		if err != nil {
			results = append(results, scanner.Result{
				Target: target,
				Status: "error",
				Error:  err.Error(),
			})
			continue
		}

		for _, ip := range ips {
			for _, port := range ports {
				wg.Add(1)
				go func(target string, ip string, port int) {
					defer wg.Done()

					sem <- struct{}{}
					defer func() {
						<-sem
					}()

					resultCh <- scanner.ScanPort(target, ip, port, timeout)
				}(target, ip, port)
			}
		}
	}

	go func() {
		wg.Wait()
		close(resultCh)
	}()

	for result := range resultCh {
		results = append(results, result)
	}

	sort.Slice(results, func(i int, j int) bool {
		if results[i].Target != results[j].Target {
			return results[i].Target < results[j].Target
		}
		if results[i].IP != results[j].IP {
			return results[i].IP < results[j].IP
		}
		return results[i].Port < results[j].Port
	})

	return results
}

В main программа просто управляет всем процессом: берет путь к файлу с целями, читает эти цели, запускает сканирование, а потом сохраняет результат в CSV-файл.

Если по шагам, то получается так: сначала проверяем, что пользователь передал -input, потом читаем список доменов или IP из файла, дальше для каждой цели получаем IP- адреса, проверяем нужные порты и в конце записываем все найденное в отчет.

Семафор здесь нужен как ограничитель. Мы запускаем много проверок портов параллельно, но не хотим, чтобы их одновременно было слишком много. Поэтому семафором говорим: “одновременно можно выполнять максимум 50 проверок”. Когда одна проверка закончилась, она освобождает место, и запускается следующая.

Отдельно я разобрал работу семафора на схеме и пошаговом примере — ссылку оставлю в конце статьи.

По сути, семафор защищает программу от ситуации, когда она сама себя перегрузит слишком большим количеством сетевых подключений.

В итоге получился небольшой TCP-сканер, который читает список целей из файла, резолвит домены в IP-адреса, проверяет набор портов с ограничением параллельности и сохраняет результат в CSV. Проект небольшой, но на нём хорошо видно, как в Go можно работать с сетью, timeout, goroutine, WaitGroup и семафором.

Ещё раз: такой инструмент стоит использовать только для своей инфраструктуры или с разрешения владельца.

Дополнительно: схема работы семафора на примере этой программы — https://t.me/walkerinit