Files
knock-gui/back/internal/knocker.go
2025-08-17 00:43:58 +06:00

643 lines
21 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package internal
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
_ "embed"
"encoding/base64"
"fmt"
"math/rand"
"net"
"os"
"strings"
"time"
"gopkg.in/yaml.v3"
)
//go:embed jokes.md
var jokesFile string
func GetRandomJoke() string {
// Инициализируем генератор случайных чисел
rand.Seed(time.Now().UnixNano())
jokes := strings.Split(jokesFile, "**********")
var cleanJokes []string
for _, joke := range jokes {
if trimmed := strings.TrimSpace(joke); trimmed != "" {
cleanJokes = append(cleanJokes, trimmed)
}
}
if len(cleanJokes) == 0 {
return "Шутки не найдены"
}
return cleanJokes[rand.Intn(len(cleanJokes))]
}
const (
// Системная переменная для ключа шифрования
EncryptionKeyEnvVar = "GO_KNOCKER_SERVE_PASS"
)
// Config представляет конфигурацию port knocking
type Config struct {
Targets []Target `yaml:"targets"`
}
// Target представляет цель для port knocking
type Target struct {
Host string `yaml:"host"`
Ports []int `yaml:"ports"`
Protocol string `yaml:"protocol"` // "tcp" или "udp"
Delay Duration `yaml:"delay"` // задержка между пакетами
WaitConnection bool `yaml:"wait_connection"` // ждать ли установления соединения
Gateway string `yaml:"gateway"` // шлюз для отправки (опционально)
}
// Duration для поддержки YAML десериализации времени
type Duration time.Duration
func (d *Duration) UnmarshalYAML(value *yaml.Node) error {
var str string
if err := value.Decode(&str); err != nil {
return err
}
duration, err := time.ParseDuration(str)
if err != nil {
return err
}
*d = Duration(duration)
return nil
}
// PortKnocker основная структура для выполнения port knocking
type PortKnocker struct{}
// NewPortKnocker создает новый экземпляр PortKnocker
func NewPortKnocker() *PortKnocker {
return &PortKnocker{}
}
// Execute выполняет port knocking на основе конфигурации
func (pk *PortKnocker) Execute(configFile, keyFile string, verbose bool, globalWaitConnection bool) error {
// Читаем конфигурацию
config, err := pk.loadConfig(configFile, keyFile)
if err != nil {
return fmt.Errorf("ошибка загрузки конфигурации: %w", err)
}
return pk.ExecuteWithConfig(config, verbose, globalWaitConnection)
}
// ExecuteWithConfig выполняет port knocking с готовой конфигурацией
func (pk *PortKnocker) ExecuteWithConfig(config *Config, verbose bool, globalWaitConnection bool) error {
if verbose {
fmt.Printf("Загружена конфигурация с %d целей\n", len(config.Targets))
}
// Выполняем port knocking для каждой цели
for i, target := range config.Targets {
if verbose {
fmt.Printf("Цель %d/%d: %s:%v (%s)\n", i+1, len(config.Targets), target.Host, target.Ports, target.Protocol)
}
// Применяем глобальный флаг если не задан локально
if globalWaitConnection && !target.WaitConnection {
target.WaitConnection = true
}
if err := pk.knockTarget(target, verbose); err != nil {
return fmt.Errorf("ошибка при knocking цели %s: %w", target.Host, err)
}
// Добавляем задержку между целями (кроме последней)
if i < len(config.Targets)-1 && target.Delay > 0 {
if verbose {
fmt.Printf("Ожидание %v перед следующей целью...\n", time.Duration(target.Delay))
}
time.Sleep(time.Duration(target.Delay))
}
}
if verbose {
fmt.Println("Port knocking завершен успешно")
}
return nil
}
// loadConfig загружает конфигурацию из файла с поддержкой шифрования
func (pk *PortKnocker) loadConfig(configFile, keyFile string) (*Config, error) {
data, err := os.ReadFile(configFile)
if err != nil {
return nil, fmt.Errorf("не удалось прочитать файл конфигурации: %w", err)
}
// Проверяем, зашифрован ли файл (начинается с "ENCRYPTED:")
if strings.HasPrefix(string(data), "ENCRYPTED:") {
fmt.Println("Обнаружен зашифрованный файл конфигурации")
// Получаем ключ шифрования
key, err := pk.getEncryptionKey(keyFile)
if err != nil {
return nil, fmt.Errorf("не удалось получить ключ шифрования: %w", err)
}
// Расшифровываем данные
decryptedData, err := pk.decrypt(data[10:], key) // пропускаем "ENCRYPTED:"
if err != nil {
return nil, fmt.Errorf("не удалось расшифровать конфигурацию: %w", err)
}
data = decryptedData
}
// Парсим YAML
var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("не удалось разобрать YAML: %w", err)
}
return &config, nil
}
// LoadConfigFromString загружает конфигурацию из строки YAML
func LoadConfigFromString(yamlStr string) (*Config, error) {
// Проверяем, зашифрована ли строка (начинается с "ENCRYPTED:")
if strings.HasPrefix(yamlStr, "ENCRYPTED:") {
// Создаем временный PortKnocker для расшифровки
pk := NewPortKnocker()
// Получаем ключ шифрования
key, err := pk.getEncryptionKey("")
if err != nil {
return nil, fmt.Errorf("не удалось получить ключ шифрования: %w", err)
}
// Расшифровываем данные
decryptedData, err := pk.decrypt([]byte(yamlStr[10:]), key) // пропускаем "ENCRYPTED:"
if err != nil {
return nil, fmt.Errorf("не удалось расшифровать конфигурацию: %w", err)
}
yamlStr = string(decryptedData)
}
// Парсим YAML
var config Config
if err := yaml.Unmarshal([]byte(yamlStr), &config); err != nil {
return nil, fmt.Errorf("не удалось разобрать YAML: %w", err)
}
return &config, nil
}
// getEncryptionKey получает ключ шифрования из файла или системной переменной и хеширует его
func (pk *PortKnocker) getEncryptionKey(keyFile string) ([]byte, error) {
var rawKey []byte
var err error
if keyFile != "" {
// Читаем ключ из файла
rawKey, err = os.ReadFile(keyFile)
if err != nil {
return nil, fmt.Errorf("не удалось прочитать файл ключа: %w", err)
}
} else {
// Пытаемся получить ключ из системной переменной
key := os.Getenv(EncryptionKeyEnvVar)
if key == "" {
return nil, fmt.Errorf("ключ шифрования не найден ни в файле, ни в переменной %s", EncryptionKeyEnvVar)
}
rawKey = []byte(key)
}
// Хешируем ключ SHA256 чтобы получить всегда 32 байта для AES-256
hash := sha256.Sum256(rawKey)
return hash[:], nil
}
// decrypt расшифровывает данные с помощью AES-GCM
func (pk *PortKnocker) decrypt(encryptedData []byte, key []byte) ([]byte, error) {
// Декодируем base64
data, err := base64.StdEncoding.DecodeString(string(encryptedData))
if err != nil {
return nil, fmt.Errorf("не удалось декодировать base64: %w", err)
}
// Создаем AES cipher
block, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("не удалось создать AES cipher: %w", err)
}
// Создаем GCM
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("не удалось создать GCM: %w", err)
}
// Извлекаем nonce
nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
return nil, fmt.Errorf("данные слишком короткие")
}
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
// Расшифровываем
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, fmt.Errorf("не удалось расшифровать: %w", err)
}
return plaintext, nil
}
// knockTarget выполняет port knocking для одной цели
func (pk *PortKnocker) knockTarget(target Target, verbose bool) error {
// Проверяем на "шутливую" цель 1
if target.Host == "8.8.8.8" && len(target.Ports) == 1 && target.Ports[0] == 8888 {
pk.showEasterEgg()
return nil
}
// Проверяем на "шутливую" цель 2
if target.Host == "1.1.1.1" && len(target.Ports) == 1 && target.Ports[0] == 1111 {
pk.showRandomJoke()
return nil
}
protocol := strings.ToLower(target.Protocol)
if protocol != "tcp" && protocol != "udp" {
return fmt.Errorf("неподдерживаемый протокол: %s", target.Protocol)
}
// Вычисляем таймаут как половину интервала между пакетами
timeout := time.Duration(target.Delay) / 2
if timeout < 100*time.Millisecond {
timeout = 100 * time.Millisecond // минимальный таймаут
}
for i, port := range target.Ports {
if verbose {
fmt.Printf(" Отправка пакета на %s:%d (%s)\n", target.Host, port, protocol)
}
if err := pk.sendPacket(target.Host, port, protocol, target.WaitConnection, timeout, target.Gateway); err != nil {
if target.WaitConnection {
return fmt.Errorf("ошибка отправки пакета на порт %d: %w", port, err)
} else {
if verbose {
fmt.Printf(" Предупреждение: не удалось отправить пакет на порт %d: %v\n", port, err)
}
}
}
// Задержка между пакетами (кроме последнего)
if i < len(target.Ports)-1 {
delay := time.Duration(target.Delay)
if delay > 0 {
if verbose {
fmt.Printf(" Ожидание %v...\n", delay)
}
time.Sleep(delay)
}
}
}
return nil
}
// sendPacket отправляет один пакет на указанный хост и порт
func (pk *PortKnocker) sendPacket(host string, port int, protocol string, waitConnection bool, timeout time.Duration, gateway string) error {
address := net.JoinHostPort(host, fmt.Sprintf("%d", port))
var conn net.Conn
var err error
// Настройка локального адреса если указан шлюз
var localAddr net.Addr
if gateway != "" {
if strings.Contains(gateway, ":") {
localAddr, err = net.ResolveTCPAddr("tcp", gateway)
if err != nil {
return fmt.Errorf("не удалось разрешить адрес шлюза %s: %w", gateway, err)
}
} else {
// Если указан только IP, добавляем порт 0
localAddr, err = net.ResolveTCPAddr("tcp", gateway+":0")
if err != nil {
return fmt.Errorf("не удалось разрешить адрес шлюза %s: %w", gateway, err)
}
}
}
switch protocol {
case "tcp":
if localAddr != nil {
dialer := &net.Dialer{
LocalAddr: localAddr,
Timeout: timeout,
}
conn, err = dialer.Dial("tcp", address)
} else {
conn, err = net.DialTimeout("tcp", address, timeout)
}
case "udp":
if localAddr != nil {
dialer := &net.Dialer{
LocalAddr: localAddr,
Timeout: timeout,
}
conn, err = dialer.Dial("udp", address)
} else {
conn, err = net.DialTimeout("udp", address, timeout)
}
default:
return fmt.Errorf("неподдерживаемый протокол: %s", protocol)
}
if err != nil {
if waitConnection {
return fmt.Errorf("не удалось подключиться к %s: %w", address, err)
} else {
// Для UDP и TCP без ожидания соединения просто отправляем пакет
return pk.sendPacketWithoutConnection(host, port, protocol, localAddr)
}
}
defer conn.Close()
// Отправляем пустой пакет
_, err = conn.Write([]byte{})
if err != nil {
return fmt.Errorf("не удалось отправить пакет: %w", err)
}
return nil
}
// sendPacketWithoutConnection отправляет пакет без установления соединения
func (pk *PortKnocker) sendPacketWithoutConnection(host string, port int, protocol string, localAddr net.Addr) error {
address := net.JoinHostPort(host, fmt.Sprintf("%d", port))
switch protocol {
case "udp":
// Для UDP просто отправляем пакет
var conn net.Conn
var err error
if localAddr != nil {
dialer := &net.Dialer{
LocalAddr: localAddr,
}
conn, err = dialer.Dial("udp", address)
} else {
conn, err = net.Dial("udp", address)
}
if err != nil {
return fmt.Errorf("не удалось создать UDP соединение к %s: %w", address, err)
}
defer conn.Close()
_, err = conn.Write([]byte{})
if err != nil {
return fmt.Errorf("не удалось отправить UDP пакет: %w", err)
}
case "tcp":
// Для TCP без ожидания соединения используем короткий таймаут
var conn net.Conn
var err error
if localAddr != nil {
dialer := &net.Dialer{
LocalAddr: localAddr,
Timeout: 100 * time.Millisecond,
}
conn, err = dialer.Dial("tcp", address)
} else {
conn, err = net.DialTimeout("tcp", address, 100*time.Millisecond)
}
if err != nil {
// Для TCP без ожидания соединения игнорируем ошибки подключения
return nil
}
defer conn.Close()
_, err = conn.Write([]byte{})
if err != nil {
return fmt.Errorf("не удалось отправить TCP пакет: %w", err)
}
}
return nil
}
// showEasterEgg показывает забавный ASCII-арт
func (pk *PortKnocker) showEasterEgg() {
fmt.Println("\n🎯 🎯 🎯 EASTER EGG ACTIVATED! 🎯 🎯 🎯")
fmt.Println()
// Анимированный ASCII-арт
frames := []string{
`
╭─────────────────╮
│ 🚀 PORT │
│ KNOCKER │
│ 🎯 1.0.1 │
│ │
│ 🎮 GAME ON! │
╰─────────────────╯
`,
`
╭─────────────────╮
│ 🚀 PORT │
│ KNOCKER │
│ 🎯 1.0.1 │
│ │
│ 🎯 BULLSEYE! │
╰─────────────────╯
`,
`
╭─────────────────╮
│ 🚀 PORT │
│ KNOCKER │
│ 🎯 1.0.1 │
│ │
│ 🎪 MAGIC! │
╰─────────────────╯
`,
}
for i := 0; i < 3; i++ {
fmt.Print("\033[2J\033[H") // Очистка экрана
fmt.Println(frames[i%len(frames)])
time.Sleep(1500 * time.Millisecond)
}
fmt.Println("\n🎉 Поздравляем! Вы нашли пасхалку!")
fmt.Println("🎯 Попробуйте: ./port-knocker -t \"tcp:8.8.8.8:8888\"")
fmt.Println("🚀 Port Knocker - теперь с пасхалками!")
fmt.Println()
}
func (pk *PortKnocker) showRandomJoke() {
joke := GetRandomJoke()
// ANSI цветовые коды
const (
colorReset = "\033[0m"
colorRed = "\033[31m"
colorGreen = "\033[32m"
colorYellow = "\033[33m"
colorBlue = "\033[34m"
colorPurple = "\033[35m"
colorCyan = "\033[36m"
colorWhite = "\033[37m"
colorBold = "\033[1m"
)
// Функция для подсчета видимой длины строки (без ANSI кодов) в рунах
visibleLength := func(s string) int {
// Удаляем ANSI escape последовательности
clean := s
for strings.Contains(clean, "\033[") {
start := strings.Index(clean, "\033[")
end := strings.Index(clean[start:], "m")
if end == -1 {
break
}
clean = clean[:start] + clean[start+end+1:]
}
// Возвращаем количество рун, а не байт
return len([]rune(clean))
}
// Функция для умного разбиения строки
splitLine := func(line string, maxWidth int) []string {
runes := []rune(line)
if len(runes) <= maxWidth {
return []string{line}
}
var result []string
remaining := line
for len([]rune(remaining)) > maxWidth {
// Ищем позицию для разрыва в пределах maxWidth
breakPos := maxWidth
remainingRunes := []rune(remaining)
for i := maxWidth; i >= 0; i-- {
if i < len(remainingRunes) {
char := remainingRunes[i]
// Разрываем на пробеле, знаке пунктуации или в конце строки
if char == ' ' || char == ',' || char == '.' || char == '!' ||
char == '?' || char == ':' || char == ';' || char == '-' {
breakPos = i + 1
break
}
}
}
// Если не нашли подходящего места, разрываем по maxWidth
if breakPos == maxWidth {
breakPos = maxWidth
}
// Создаем строку из рун
breakString := string(remainingRunes[:breakPos])
result = append(result, strings.TrimSpace(breakString))
remaining = strings.TrimSpace(string(remainingRunes[breakPos:]))
}
if len([]rune(remaining)) > 0 {
result = append(result, remaining)
}
return result
}
// Разбиваем исходную шутку на строки
originalLines := strings.Split(joke, "\n")
// Обрабатываем каждую строку и разбиваем длинные
var processedLines []string
for _, line := range originalLines {
if strings.TrimSpace(line) == "" {
continue
}
splitLines := splitLine(line, 80)
processedLines = append(processedLines, splitLines...)
}
// Находим максимальную длину строки для рамки (в рунах)
maxLength := 0
for _, line := range processedLines {
lineLength := len([]rune(line))
if lineLength > maxLength {
maxLength = lineLength
}
}
// Убеждаемся, что maxLength не меньше минимальной ширины для заголовков
minWidth := 60 // Минимальная ширина для заголовков
if maxLength < minWidth {
maxLength = minWidth
}
fmt.Println()
fmt.Printf("%s%s╭%s", colorPurple, colorBold, colorReset)
fmt.Printf("%s%s", colorYellow, strings.Repeat("─", maxLength+2))
fmt.Printf("%s%s╮%s\n", colorPurple, colorBold, colorReset)
headerText := " Зацени Анектотец! 🤣 "
fmt.Printf("%s%s│%s", colorPurple, colorBold, colorReset)
fmt.Printf("%s%s%s%s", colorCyan, colorBold, headerText, colorReset)
fmt.Printf("%s%s", colorYellow, strings.Repeat(" ", 1+maxLength-visibleLength(headerText)))
fmt.Printf("%s%s│%s\n", colorPurple, colorBold, colorReset)
fmt.Printf("%s%s├%s", colorPurple, colorBold, colorReset)
fmt.Printf("%s%s", colorYellow, strings.Repeat("─", maxLength+2))
fmt.Printf("%s%s┤%s\n", colorPurple, colorBold, colorReset)
// Выводим обработанные строки шутки
for _, line := range processedLines {
fmt.Printf("%s%s│%s", colorPurple, colorBold, colorReset)
fmt.Printf("%s%s%s", colorWhite, line, colorReset)
fmt.Printf("%s%s", colorYellow, strings.Repeat(" ", 2+maxLength-len([]rune(line))))
fmt.Printf("%s%s│%s\n", colorPurple, colorBold, colorReset)
}
fmt.Printf("%s%s├%s", colorPurple, colorBold, colorReset)
fmt.Printf("%s%s", colorYellow, strings.Repeat("─", maxLength+2))
fmt.Printf("%s%s┤%s\n", colorPurple, colorBold, colorReset)
// Вычисляем правильную ширину для нижних строк
cmdText := "Попробуйте: ./port-knocker -t \"tcp:1.1.1.1:1111\""
titleText := "🚀 Port Knocker - теперь с шутками! 🤣"
fmt.Printf("%s%s│%s", colorPurple, colorBold, colorReset)
fmt.Printf("%s%s%s%s", colorGreen, colorBold, cmdText, colorReset)
fmt.Printf("%s%s", colorYellow, strings.Repeat(" ", 2+maxLength-visibleLength(cmdText)))
fmt.Printf("%s%s│%s\n", colorPurple, colorBold, colorReset)
fmt.Printf("%s%s│%s", colorPurple, colorBold, colorReset)
fmt.Printf("%s%s%s%s", colorBlue, colorBold, titleText, colorReset)
fmt.Printf("%s%s", colorYellow, strings.Repeat(" ", maxLength-visibleLength(titleText)))
fmt.Printf("%s%s│%s\n", colorPurple, colorBold, colorReset)
fmt.Printf("%s%s╰%s", colorPurple, colorBold, colorReset)
fmt.Printf("%s%s", colorYellow, strings.Repeat("─", maxLength+2))
fmt.Printf("%s%s╯%s\n", colorPurple, colorBold, colorReset)
fmt.Println()
}