This commit is contained in:
66 changed files with 19815 additions and 0 deletions

642
back/internal/knocker.go Normal file
View File

@@ -0,0 +1,642 @@
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()
}