From 2012fb3afc41a524cdb769cfefa4fa85bc3c8172 Mon Sep 17 00:00:00 2001 From: Anton Kuznetcov Date: Thu, 11 Sep 2025 14:29:05 +0600 Subject: [PATCH] =?UTF-8?q?feat(knocker):=20=D1=80=D0=B5=D0=B7=D0=B5=D1=80?= =?UTF-8?q?=D0=B2=D0=BD=D1=8B=D0=B5=20=D0=BA=D0=BE=D0=BF=D0=B8=D0=B8=20sen?= =?UTF-8?q?dPacket*/sendPacketWithoutConnection*=20=D0=B8=20=D0=BE=D0=B1?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5:=20UDPAddr=20+?= =?UTF-8?q?=20SO=5FBINDTODEVICE=20(gateway=20=D0=BF=D0=BE=D0=B4=D0=B4?= =?UTF-8?q?=D0=B5=D1=80=D0=B6=D0=B8=D0=B2=D0=B0=D0=B5=D1=82=20IP[:port]=20?= =?UTF-8?q?=D0=B8=D0=BB=D0=B8=20=D0=B8=D0=BC=D1=8F=20=D0=B8=D0=BD=D1=82?= =?UTF-8?q?=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=D0=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- back/internal/knocker.go | 166 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 159 insertions(+), 7 deletions(-) diff --git a/back/internal/knocker.go b/back/internal/knocker.go index b946717..b37d4fe 100644 --- a/back/internal/knocker.go +++ b/back/internal/knocker.go @@ -10,9 +10,12 @@ import ( "math/rand" "net" "os" + "regexp" "strings" + "syscall" "time" + "golang.org/x/sys/unix" "gopkg.in/yaml.v3" ) @@ -278,10 +281,9 @@ func (pk *PortKnocker) knockTarget(target Target, verbose bool) error { } // Вычисляем таймаут как половину интервала между пакетами - timeout := time.Duration(target.Delay) / 2 - if timeout < 100*time.Millisecond { - timeout = 100 * time.Millisecond // минимальный таймаут - } + timeout := max(time.Duration(target.Delay)/2, + // минимальный таймаут + 100*time.Millisecond) for i, port := range target.Ports { if verbose { @@ -314,7 +316,8 @@ func (pk *PortKnocker) knockTarget(target Target, verbose bool) error { } // 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)) var conn net.Conn @@ -381,8 +384,8 @@ func (pk *PortKnocker) sendPacket(host string, port int, protocol string, waitCo return nil } -// sendPacketWithoutConnection отправляет пакет без установления соединения -func (pk *PortKnocker) sendPacketWithoutConnection(host string, port int, protocol string, localAddr net.Addr) error { +// sendPacketWithoutConnection_backup — резервная копия прежней реализации +func (pk *PortKnocker) SendPacketWithoutConnection_backup(host string, port int, protocol string, localAddr net.Addr) error { address := net.JoinHostPort(host, fmt.Sprintf("%d", port)) switch protocol { @@ -440,6 +443,155 @@ func (pk *PortKnocker) sendPacketWithoutConnection(host string, port int, protoc 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-арт func (pk *PortKnocker) showEasterEgg() { fmt.Println("\n🎯 🎯 🎯 EASTER EGG ACTIVATED! 🎯 🎯 🎯")