feat(knocker): резервные копии sendPacket*/sendPacketWithoutConnection* и обновление: UDPAddr + SO_BINDTODEVICE (gateway поддерживает IP[:port] или имя интерфейса)

This commit is contained in:
2025-09-11 14:29:05 +06:00
parent 538804b0ac
commit 2012fb3afc

View File

@@ -10,9 +10,12 @@ import (
"math/rand" "math/rand"
"net" "net"
"os" "os"
"regexp"
"strings" "strings"
"syscall"
"time" "time"
"golang.org/x/sys/unix"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -278,10 +281,9 @@ func (pk *PortKnocker) knockTarget(target Target, verbose bool) error {
} }
// Вычисляем таймаут как половину интервала между пакетами // Вычисляем таймаут как половину интервала между пакетами
timeout := time.Duration(target.Delay) / 2 timeout := max(time.Duration(target.Delay)/2,
if timeout < 100*time.Millisecond { // минимальный таймаут
timeout = 100 * time.Millisecond // минимальный таймаут 100*time.Millisecond)
}
for i, port := range target.Ports { for i, port := range target.Ports {
if verbose { if verbose {
@@ -314,7 +316,8 @@ func (pk *PortKnocker) knockTarget(target Target, verbose bool) error {
} }
// sendPacket отправляет один пакет на указанный хост и порт // sendPacket отправляет один пакет на указанный хост и порт
func (pk *PortKnocker) sendPacket(host string, port int, protocol string, waitConnection bool, timeout time.Duration, gateway string) error { // sendPacket_backup — резервная копия прежней реализации
func (pk *PortKnocker) SendPacket_backup(host string, port int, protocol string, waitConnection bool, timeout time.Duration, gateway string) error {
address := net.JoinHostPort(host, fmt.Sprintf("%d", port)) address := net.JoinHostPort(host, fmt.Sprintf("%d", port))
var conn net.Conn var conn net.Conn
@@ -381,8 +384,8 @@ func (pk *PortKnocker) sendPacket(host string, port int, protocol string, waitCo
return nil return nil
} }
// sendPacketWithoutConnection отправляет пакет без установления соединения // sendPacketWithoutConnection_backup — резервная копия прежней реализации
func (pk *PortKnocker) sendPacketWithoutConnection(host string, port int, protocol string, localAddr net.Addr) error { func (pk *PortKnocker) SendPacketWithoutConnection_backup(host string, port int, protocol string, localAddr net.Addr) error {
address := net.JoinHostPort(host, fmt.Sprintf("%d", port)) address := net.JoinHostPort(host, fmt.Sprintf("%d", port))
switch protocol { switch protocol {
@@ -440,6 +443,155 @@ func (pk *PortKnocker) sendPacketWithoutConnection(host string, port int, protoc
return nil return nil
} }
// parseGateway возвращает локальные адреса для TCP/UDP и (опционально) имя интерфейса,
// если в gateway передано, например, "eth1". Также поддерживается IP[:port].
func parseGateway(gateway string) (tcpLocal *net.TCPAddr, udpLocal *net.UDPAddr, ifaceName string, err error) {
gateway = strings.TrimSpace(gateway)
if gateway == "" {
return nil, nil, "", nil
}
// Если это похоже на имя интерфейса (буквы/цифры/дефисы, без точного IP)
isIfaceName := regexp.MustCompile(`^[A-Za-z0-9_-]+$`).MatchString(gateway) && net.ParseIP(gateway) == nil
if isIfaceName {
// Привязка по интерфейсу. LocalAddr оставим пустым — маршрут выберется ядром,
// а SO_BINDTODEVICE закрепит интерфейс.
return nil, nil, gateway, nil
}
// Иначе трактуем как IP[:port]
host := gateway
if !strings.Contains(gateway, ":") {
host = gateway + ":0"
}
tcpLocal, err = net.ResolveTCPAddr("tcp", host)
if err != nil {
return nil, nil, "", fmt.Errorf("не удалось разрешить локальный TCP адрес %s: %w", host, err)
}
udpLocal, err = net.ResolveUDPAddr("udp", host)
if err != nil {
return nil, nil, "", fmt.Errorf("не удалось разрешить локальный UDP адрес %s: %w", host, err)
}
return tcpLocal, udpLocal, "", nil
}
// buildDialer создаёт net.Dialer с опциональной привязкой к LocalAddr и интерфейсу (Linux SO_BINDTODEVICE)
func buildDialer(protocol string, tcpLocal *net.TCPAddr, udpLocal *net.UDPAddr, timeout time.Duration, ifaceName string) *net.Dialer {
d := &net.Dialer{Timeout: timeout}
if protocol == "tcp" && tcpLocal != nil {
d.LocalAddr = tcpLocal
}
if protocol == "udp" && udpLocal != nil {
d.LocalAddr = udpLocal
}
if strings.TrimSpace(ifaceName) != "" {
d.Control = func(network, address string, c syscall.RawConn) error {
var ctrlErr error
err := c.Control(func(fd uintptr) {
// Привязка сокета к интерфейсу по имени
ctrlErr = unix.SetsockoptString(int(fd), unix.SOL_SOCKET, unix.SO_BINDTODEVICE, ifaceName)
})
if err != nil {
return err
}
return ctrlErr
}
}
return d
}
// sendPacket — обновлённая реализация с поддержкой UDPAddr и SO_BINDTODEVICE
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))
tcpLocal, udpLocal, ifaceName, err := parseGateway(gateway)
if err != nil {
return err
}
switch protocol {
case "tcp":
dialer := buildDialer("tcp", tcpLocal, nil, timeout, ifaceName)
conn, err := dialer.Dial("tcp", address)
if err != nil {
if waitConnection {
return fmt.Errorf("не удалось подключиться к %s: %w", address, err)
}
// без ожидания — пробуем best-effort отправку
return pk.sendPacketWithoutConnection(host, port, protocol, dialer.LocalAddr)
}
defer conn.Close()
_, err = conn.Write([]byte{})
if err != nil {
return fmt.Errorf("не удалось отправить пакет: %w", err)
}
return nil
case "udp":
dialer := buildDialer("udp", nil, udpLocal, timeout, ifaceName)
conn, err := dialer.Dial("udp", address)
if err != nil {
if waitConnection {
return fmt.Errorf("не удалось подключиться к %s: %w", address, err)
}
return pk.sendPacketWithoutConnection(host, port, protocol, dialer.LocalAddr)
}
defer conn.Close()
_, err = conn.Write([]byte{})
if err != nil {
return fmt.Errorf("не удалось отправить пакет: %w", err)
}
return nil
default:
return fmt.Errorf("неподдерживаемый протокол: %s", protocol)
}
}
// sendPacketWithoutConnection — обновлённая реализация best-effort без ожидания соединения
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":
// Используем Dialer с коротким таймаутом, локальный адрес может быть *net.UDPAddr
var udpLocal *net.UDPAddr
if la, ok := localAddr.(*net.UDPAddr); ok {
udpLocal = la
}
d := &net.Dialer{Timeout: 200 * time.Millisecond}
if udpLocal != nil {
d.LocalAddr = udpLocal
}
conn, err := d.Dial("udp", address)
if err != nil {
return nil
} // best-effort
defer conn.Close()
_, err = conn.Write([]byte{})
if err != nil {
return nil
}
case "tcp":
// Короткий таймаут и игнор ошибок
var tcpLocal *net.TCPAddr
if la, ok := localAddr.(*net.TCPAddr); ok {
tcpLocal = la
}
d := &net.Dialer{Timeout: 100 * time.Millisecond}
if tcpLocal != nil {
d.LocalAddr = tcpLocal
}
conn, err := d.Dial("tcp", address)
if err != nil {
return nil
}
defer conn.Close()
_, _ = conn.Write([]byte{})
}
return nil
}
// showEasterEgg показывает забавный ASCII-арт // showEasterEgg показывает забавный ASCII-арт
func (pk *PortKnocker) showEasterEgg() { func (pk *PortKnocker) showEasterEgg() {
fmt.Println("\n🎯 🎯 🎯 EASTER EGG ACTIVATED! 🎯 🎯 🎯") fmt.Println("\n🎯 🎯 🎯 EASTER EGG ACTIVATED! 🎯 🎯 🎯")