Горячее
Лучшее
Свежее
Подписки
Сообщества
Блоги
Эксперты
Войти
Забыли пароль?
или продолжите с
Создать аккаунт
Я хочу получать рассылки с лучшими постами за неделю
или
Восстановление пароля
Восстановление пароля
Получить код в Telegram
Войти с Яндекс ID Войти через VK ID
Создавая аккаунт, я соглашаюсь с правилами Пикабу и даю согласие на обработку персональных данных.
ПромокодыРаботаКурсыРекламаИгрыПополнение Steam

Пикабу Игры +1000 бесплатных онлайн игр

Возглавьте отряд стражей, чтобы изгнать злого чародея Харона. Отправляйтесь в опасное путешествие, полное приключений. Играть можно онлайн абсолютно бесплатно и без регистрации.

Королевство Дом

Казуальные, Настольные, Стратегии

Играть
Реалистичный симулятор игры бильярд. Играйте в Бильярд 3D: Русский бильярд, как в настоящей бильярдной!

Бильярд 3D: Русский бильярд

Симуляторы, Спорт, Настольные

Играть
Открой для себя волшебный мир реальной рыбалки. Лови реальную рыбу на реальных водоемах! Исследуй новые рыболовные места и заполучи заветный трофей.

Реальная Рыбалка

Симуляторы, Мультиплеер, Спорт

Играть
Решай головоломку с котиками!

Котолэнд: блок пазл

Головоломки, Казуальные, 2D

Играть

Тайна Самоцветов: Ключ Сокровищ - Три в ряд

Казуальные, Три в ряд, Головоломки

Играть

Топ прошлой недели

  • Oskanov Oskanov 8 постов
  • alekseyJHL alekseyJHL 6 постов
  • XpyMy XpyMy 1 пост
Посмотреть весь топ

Лучшие посты недели

Рассылка Пикабу: отправляем самые рейтинговые материалы за 7 дней 🔥

Нажимая кнопку «Подписаться на рассылку», я соглашаюсь с Правилами Пикабу и даю согласие на обработку персональных данных.

Спасибо, что подписались!
Пожалуйста, проверьте почту 😊

Новости Пикабу Помощь Кодекс Пикабу Реклама О компании
Команда Пикабу Награды Контакты О проекте Зал славы
Промокоды Скидки Работа Курсы Блоги
Купоны Biggeek Купоны AliExpress Купоны М.Видео Купоны YandexTravel Купоны Lamoda
Мобильное приложение

Golang

С этим тегом используют

Программирование IT Программист Разработка Все
82 поста сначала свежее
FreshAngry007
1 год назад

Go и Generics⁠⁠

Дженерики (обобщенное программирование) были введены в язык программирования Go в версии 1.18. Они позволяют писать функции и типы данных, которые могут работать с различными типами данных без потери типобезопасности. До появления дженериков разработчики Go часто использовали интерфейсы и тип interface{} для создания функций и структур, способных работать с любыми типами данных, что могло привести к ошибкам времени выполнения из-за неправильного преобразования типов.

Синтаксис

Дженерики в Go определяются с использованием квадратных скобок [] после имени функции или типа. Внутри скобок указываются один или несколько типовых параметров, которые затем можно использовать в теле функции или типа как обычные типы данных.

Go и Generics Программирование, IT, Golang, Длиннопост

В этом примере T является типовым параметром, который может быть заменен любым типом данных. Ключевое слово any означает, что T может быть любым типом.

Преимущества

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

  • Типобезопасность: В отличие от использования interface{}, дженерики обеспечивают проверку типов на этапе компиляции, что уменьшает риск ошибок времени выполнения.

  • Производительность: Использование дженериков может улучшить производительность, так как избавляет от необходимости преобразования типов и позволяет компилятору оптимизировать генерируемый код.

Ограничения

  • Синтаксис дженериков может быть сложным для понимания, особенно для начинающих разработчиков.

  • Обобщенное программирование может усложнить структуру кода и его читаемость.

  • Не все существующие библиотеки и инструменты уже полностью поддерживают дженерики.

Несмотря на ограничения, введение дженериков в Go значительно улучшило возможности языка для создания более гибкого и безопасного кода.

Следующий код демонстрирует применение обобщенного программирования в Go с использованием типового параметра T, ограниченного интерфейсом Signed. Интерфейс Signed определяет типы, которые могут быть одним из подписанных (signed) целочисленных типов (int, int8, int16, int32, int64). Структура NumericBox использует этот типовый параметр, что позволяет создавать экземпляры NumericBox для любого подписанного целочисленного типа.

Go и Generics Программирование, IT, Golang, Длиннопост

package main

import (

"fmt"

)

type Signed interface {

~int | ~int8 | ~int16 | ~int32 | ~int64

}

// NumericBox - обобщенная структура, ограниченная для работы только с числовыми типами.

type NumericBox[T Signed] struct {

Value T

}

// NewNumericBox создает новый экземпляр NumericBox с заданным числовым значением.

func NewNumericBox[T Signed](value T) NumericBox[T] {

return NumericBox[T]{Value: value}

}

// GetValue возвращает числовое значение из NumericBox.

func (b NumericBox[T]) GetValue() T {

return b.Value

}

func main() {

int_32 := NewNumericBox(int32(123))

fmt.Println("IntBox:", int_32.GetValue()) // Вывод: IntBox: 123

int_64 := NewNumericBox(int64(-123))

fmt.Println("IntBox:", int_64.GetValue()) // Вывод: IntBox: -123

// Следующая строка вызовет ошибку компиляции, так как строка не является числовым типом.

// stringBox := NewNumericBox("Hello, Generics!") // string does not satisfy Signed (string missing in ~int | ~int8 | ~int16 | ~int32 | ~int64)

}

Давайте теперь напишем замер скорости работы, простой структуры, интерфейса и Ginerics

Go и Generics Программирование, IT, Golang, Длиннопост

package main

import (

"fmt"

"time"

)

type Animal interface { Get() int }

type Dog struct{ Value int }

func (d Dog) Get() int { return d.Value }

type Signed interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 }

type NumericBox[T Signed] struct { Value T }

// Функция, принимающая конкретный тип.

func printInt(i int) {

fmt.Println(i)

}

// функция, принимающая интерфейс Animal.

func printInterface(animal Animal) {

fmt.Println(animal.Get())

}

// Функция, принимающая экземпляр NumericBox с любым поддерживаемым числовым типом.

func printGinerics[T Signed](box NumericBox[T]) {

fmt.Println(box.Value)

}

func main() {

// Замер производительности при использовании конкретного типа.

startConcrete := time.Now()

for i := 0; i < 1000000; i++ {

printInt(i)

}

resultConcrete := time.Since(startConcrete)

// Замер производительности при использовании интерфейса.

startInter := time.Now()

for i := 0; i < 1000000; i++ {

printInterface(Dog{Value: i})

}

resultInter := time.Since(startInter)

// Замер производительности при использовании дженерика.

startGeneric := time.Now()

for i := 0; i < 1000000; i++ {

printGinerics(NumericBox[int]{Value: i})

}

resultGeneric := time.Since(startGeneric)

fmt.Printf("Using concrete type: %v\n", resultConcrete) // Using concrete type: 869.528353ms

fmt.Printf("Using interface: %v\n", resultInter) // Using interface: 877.926898ms

fmt.Printf("Using generic: %v\n", resultGeneric) // Using generic: 841.916956ms

}

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

  1. Меньшее количество преобразований типов: Когда вы используете дженерики, Go может генерировать код непосредственно для конкретного типа во время компиляции, что уменьшает необходимость в преобразованиях типов или упаковке/распаковке интерфейсов во время выполнения. Это делает операции более быстрыми.

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

  3. Отсутствие динамического поиска методов: Когда вы используете интерфейсы, Go должен выполнить поиск соответствующего метода в таблице виртуальных методов интерфейса во время выполнения. Дженерики же позволяют избежать этого, так как конкретная реализация метода известна на этапе компиляции.

  4. Компилятор оптимизирует код для дженериков: Компилятор Go может выполнять специализацию кода для дженериков, что означает, что он может генерировать оптимизированный код для каждого конкретного случая использования.

Показать полностью 3
[моё] Программирование IT Golang Длиннопост
5
FreshAngry007
1 год назад

Go и интерфейсы⁠⁠

В Go интерфейсы представляют собой типы, определяющие наборы методов. Они используются для обеспечения полиморфного поведения объектов. В отличие от многих других языков программирования, в Go не требуется явное указание на то, что структура реализует интерфейс. Если структура имеет все методы, описанные в интерфейсе, то считается, что она его реализует.

Основные принципы работы интерфейсов:

  1. Определение интерфейса: Интерфейс в Go описывается как набор методов. Структура или любой другой тип "реализует" интерфейс, если предоставляет реализацию всех методов, перечисленных в интерфейсе.

  2. Полиморфизм: Интерфейсы позволяют переменным иметь различные типы значения во время выполнения программы, что обеспечивает полиморфизм. Это значит, что функция, принимающая интерфейс в качестве аргумента, может работать с любым типом, который реализует этот интерфейс.

  3. Интерфейс{}: Пустой интерфейс interface{} не имеет методов и поэтому может представлять значение любого типа, включая встроенные типы данных. Он часто используется, когда точный тип данных заранее неизвестен.

Go и интерфейсы Golang, Программирование, IT, Интерфейс, Длиннопост

package main

import "fmt"

// Определение интерфейса Animal

type Animal interface {

Speak() string

}

// Реализация интерфейса Animal для структуры Dog

type Dog struct {}

func (d Dog) Speak() string {

return "Woof!"

}

// Реализация интерфейса Animal для структуры Cat

type Cat struct {}

func (c Cat) Speak() string {

return "Meow"

}

// Функция, принимающая интерфейс Animal

func printAnimalSound(a Animal) {

fmt.Println(a.Speak())

}

func main() {

dog := Dog{}

cat := Cat{}


// Оба типа, Dog и Cat, реализуют интерфейс Animal

// поэтому printAnimalSound может с ними работать

printAnimalSound(dog) // Woof!

printAnimalSound(cat) // Meow

}

В этом примере Dog и Cat являются разными типами, которые реализуют интерфейс Animal, поскольку оба предоставляют метод Speak(). Функция printAnimalSound может принимать любой тип, реализующий Animal.

Подводные камни.

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

Go и интерфейсы Golang, Программирование, IT, Интерфейс, Длиннопост

package main


import (

"fmt"

"time"

)


// Animal интерфейс, который реализуют разные животные.

type Animal interface {

Say() string

}


// Dog структура, реализующая интерфейс Animal.

type Dog struct{}


func (d Dog) Say() string {

return "Woof!"

}


// функция, принимающая интерфейс Animal.

func makeNoise(animal Animal) {

fmt.Println(animal.Say())

}


// Функция, принимающая конкретный тип Dog.

func makeDogNoise(dog Dog) {

fmt.Println(dog.Say())

}


func main() {


dog := Dog{}


// Замер производительности при использовании интерфейса.

start_inter := time.Now()

for i := 0; i < 1000000; i++ {

makeNoise(dog)

}

result_iter := time.Since(start_inter)


// Замер производительности при использовании конкретного типа.

start := time.Now()

for i := 0; i < 1000000; i++ {

makeDogNoise(dog)

}

result_concrete := time.Since(start)


fmt.Printf("Using interface: %v\n", result_iter) // Using interface: 841.732596ms

fmt.Printf("Using concrete type: %v\n", result_concrete) // Using concrete type: 822.222233ms


}

В этом примере мы сравниваем время выполнения функций, которые работают с интерфейсом Animal и с конкретным типом Dog. Хотя разница в производительности может быть незначительной для малого количества вызовов, в высокопроизводительных системах или в системах с большим количеством вызовов эта разница может накапливаться.

Преимущества.

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

  2. Отделение интерфейса от реализации: Интерфейсы позволяют скрыть детали реализации за абстракцией. Это упрощает понимание кода, поскольку вам нужно сосредоточиться только на том, что делает код, а не как он это делает.

  3. Упрощение тестирования: Использование интерфейсов упрощает написание модульных тестов. Вы можете легко создать "моки" или фиктивные реализации интерфейсов для тестирования, не завися от реализаций, которые могут требовать внешних зависимостей (например, базы данных).

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

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

Показать полностью 2
Golang Программирование IT Интерфейс Длиннопост
0
FreshAngry007
1 год назад

Структуры и Go⁠⁠

В Go структуры являются композитным типом данных, позволяющим объединять данные различных типов в одну логическую группу. Структуры часто используются для представления объектов и записей.

Вот основы работы со структурами:

Структуры и Go Golang, Программирование, Структура, IT, Длиннопост

package main

import (

"fmt"

)

// Этот код определяет структуру Person с 3-мя полями: Name (строка), Age (целое число) и hidden (булево значение).

// Приватные поля в Go определяются с помощью нижнего регистра первой буквы имени поля в структуре.

// Они не доступны за пределами пакета, в котором объявлена структура.

type Person struct {

hidden bool

Name string

Age  int

}

// К структурам можно привязывать методы, используя получатель (receiver):

func (p *Person) Greet() string {

return "Hello, " + p.Name

}

// Методы структуры могут иметь доступ к её приватным полям, что позволяет вам управлять доступом к этим полям через публичные методы.

func (e *Person) SetPrivateField(value bool) {

e.hidden = value

}

func (e *Person) GetPrivateField() bool {

return e.hidden

}

// В Go поддерживается встраивание структур что позволяет создавать сложные структуры данных и реализовывать наследование в стиле композиции

// Экземпляр Employee наследует поля и методы Person, к ним можно обращаться так, как если бы они были объявлены напрямую в Employee.

type Employee struct {

Person

Position string

}

func main() {

// Экземпляр структуры может быть создан следующим образом

p := Person{false, "John Doe", 30}


// или с использованием именованных полей для повышения читаемости

p = Person{Name: "John Doe", Age: 30}


// Доступ к полям структуры осуществляется через оператор точки

fmt.Println(p.Name) // Выведет: John Doe


// Вызов метода осуществляется так же, как и доступ к полям

message := p.Greet()

fmt.Println(message) // Выведет: Hello, John Doe


// Go поддерживает анонимные структуры, которые могут быть полезны для одноразовых структур данных:

var user = struct {

ID  int

Name string

}{ID: 1, Name: "John Doe"}

fmt.Println(user) // Выведет: {1 John Doe}

}

В Go методы структур могут иметь два типа приемников (receiver): по значению (копия структуры) и по указателю. Выбор между ними влияет на возможность метода изменять саму структуру и на производительность. Вот основные различия:

Приемник по значению (копия)

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

Структуры и Go Golang, Программирование, Структура, IT, Длиннопост

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

Приемник по указателю

Когда метод принимает структуру по указателю, он может изменять саму структуру, поскольку работает не с копией, а с реальным адресом структуры в памяти.

Структуры и Go Golang, Программирование, Структура, IT, Длиннопост

Использование приемника по указателю рекомендуется, когда:

  • Необходимо модифицировать саму структуру в методе.

  • Структура достаточно большая, и копирование её значений могло бы отрицательно сказаться на производительности.

Подводные камни

Структуры и Go Golang, Программирование, Структура, IT, Длиннопост

package main

import "fmt"

type Example struct {

Count int

Name string

}

type Derived struct {

Example

Name string

}

type ExampleP struct {

Example *Example

}

func main() {

// Значение по умолчанию для структур

var ex Example

fmt.Println(ex.Count) // Выведет 0, значение по умолчанию для int

fmt.Println(ex.Name)  // Выведет "", значение по умолчанию для string

// Встраивание структур и теневая реализация

d := Derived{}

d.Name = "Derived"

d.Example.Name = "Base"

fmt.Println(d.Name, d.Example.Name) // Чтобы получить доступ к полю Name из встроенной структуры Example, необходимо явно указать путь к нему

// Неинициализированные указатели в структурах

exp := ExampleP{}

fmt.Println(exp.Example.Name) // Приведет к панике, так как ex.Nested == nil

// Сравнение структур

// Не будет работать для структур, содержащих срезы!

ex1 := Example{Count: 5}

ex2 := Example{Count: 5}

fmt.Println(ex1 == ex2) // Выведет "true", так как все поля совпадают

}

В Go, сравнение структур с помощью оператора == будет работать только если все поля структуры поддерживают операцию сравнения. Это означает, что структуры, содержащие следующие типы данных в качестве полей, не могут быть напрямую сравнены:

  1. Срезы (slices): Как вы уже заметили, срезы не поддерживают операцию сравнения. Попытка сравнить структуры, содержащие срезы, приведет к компиляционной ошибке.

  2. Мапы (maps): Также как срезы, мапы не поддерживают операцию сравнения. Структуры с полями типа мапа не могут быть сравнены с помощью ==.

  3. Функции (functions): Функции в Go также не могут быть сравнены напрямую, кроме сравнения на равенство или неравенство с nil.

  4. Каналы (channels): Хотя каналы могут быть сравнены на равенство или неравенство, включая сравнение с nil, сложные сравнения структур, содержащих каналы, могут быть неоднозначными в зависимости от конкретной логики.

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

Если вам нужно сравнить структуры, содержащие такие типы данных, вам придется реализовать собственную логику сравнения, которая будет обходиться с каждым полем индивидуально, в зависимости от его типа и предполагаемого значения.

В Go, когда вы передаете структуру в функцию, по умолчанию происходит её копирование. Это значит, что внутри функции создается локальная копия структуры, и любые изменения, сделанные в этой копии, не отразятся на оригинальной структуре. Это поведение соответствует передаче аргументов по значению.

Структуры и Go Golang, Программирование, Структура, IT, Длиннопост
Показать полностью 5
[моё] Golang Программирование Структура IT Длиннопост
21
FreshAngry007
1 год назад

Карты и Go⁠⁠

Карты в Go — это встроенный тип данных, предоставляющий возможность хранения пар ключ-значение. Они похожи на словари или ассоциативные массивы в других языках программирования.

Особенности карт в Go:

  • Динамический размер: Карты могут расти и уменьшаться по мере добавления и удаления элементов, что делает их гибкими для использования в ситуациях, когда количество элементов заранее неизвестно.

  • Типизированные ключи и значения: В картах Go и ключи, и значения могут быть почти любого типа, за исключением тех, которые содержат срезы, карты или другие несравнимые типы. Тип ключа и тип значения для карты указываются при её объявлении.

  • Быстрый доступ: Карты предоставляют быстрый доступ к значениям по ключам. Время доступа к элементу карты не зависит от размера карты, что делает их эффективным выбором для поиска данных.

Работа с картами:

Карты и Go Golang, Программирование, Карты, Длиннопост

package main

import (

"fmt"

)

func main() {

// Объявление карты

var myMap map[int]string

fmt.Println(myMap) // map[]

// Инициализация карты

myMap = make(map[int]string)

fmt.Println(myMap) // map[]

// Или можно объявить и инициализировать карту одновременно

myMap = make(map[int]string)

fmt.Println(myMap) // map[]

/*

Или можно объявить и инициализировать карту одновременно с емкостью


В отличие от слайсов, у карт нет понятия внешней емкости, доступной для проверки с помощью функции cap().

Это означает, что вызов cap(myMap) будет ошибочным, так как функция cap() не применима к картам.


Начальная емкость карты в Go служит лишь подсказкой для реализации о том, сколько элементов ожидается в карте.

Это может помочь оптимизировать ее внутреннюю структуру и уменьшить количество аллокаций памяти в

начальной фазе использования карты.

*/

myMap = make(map[int]string, 5)

fmt.Println(myMap, len(myMap)) // map[] 0


// Или можно объявить и инициализировать карту одновременно значениями

myMap = map[int]string{1: "a", 2: "b", 3: "c"}

fmt.Println(myMap) // map[1:a 2:b 3:c]

key := 5

// Добавить или изменить значение для ключа "apple"

myMap[key] = "apple"

fmt.Println(myMap) // map[1:a 2:b 3:c 5:apple]

// Получить значение. Если ключ существует, `ok` будет true, иначе false.

if _, ok := myMap[key]; !ok {

panic(fmt.Sprintf("myMap: key %d not found", key)) // panic: myMap: key 6 not found, если ключа 6 не окажется в карте

}

// Удалить элемент с ключом "apple"

// Ну и ни чего не происходит если такого ключа нет

delete(myMap, 6)

fmt.Println(myMap) // map[1:a 2:b 3:c 5:apple]

// Итерация по карте

for k, v := range myMap {

fmt.Println(k, v)

}

}

Подводные камни

1. Изменение карты во время итерации

Модификация карты во время итерации по ней может привести к непредсказуемому поведению. Например, вы можете пропустить некоторые элементы или столкнуться с паникой в рантайме.

Это допустимо в Go и не приведет к непосредственной ошибке или панике.

В случае с Go, поведение при удалении элементов из карты во время итерации по ней довольно определенное и безопасное. Итератор карты в Go спроектирован так, чтобы обрабатывать изменения карты во время итерации, но это может привести к тому, что некоторые элементы будут пропущены в процессе итерации, если вы удаляете элементы впереди текущего элемента итерации.

Карты и Go Golang, Программирование, Карты, Длиннопост

package main

func main() {

m := map[int]string{1: "a", 2: "b", 3: "c"}

for k := range m {

if k == 2 {

delete(m, 3) // Попытка удалить ключ во время итерации

}

}

// Поведение не определено. Возможно пропуск элементов или другие непредвиденные результаты.

}

Решение.

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

Карты и Go Golang, Программирование, Карты, Длиннопост

package main

import "fmt"

func main() {

m := map[int]string{1: "a", 2: "b", 3: "c"}

var keysToDelete []int

for k := range m {

if k == 2 {


// добавляем ключи для удаления

keysToDelete = append(keysToDelete, 3)

}

}

// Удаляем элементы после итерации

for _, k := range keysToDelete {

delete(m, k)

}

fmt.Println(m) // Вывод: map[1:a 2:b], элемент с ключом 3 удален

}

2. Неинициализированная карта

Попытка использования неинициализированной карты вызовет панику во время выполнения.

Карты и Go Golang, Программирование, Карты, Длиннопост

package main

func main() {

// Попытка использования неинициализированной карты вызовет панику во время выполнения.

var m map[int]string

m[1] = "apple" // panic: assignment to entry in nil map

// Чтобы избежать этого, карта должна быть инициализирована перед использованием

m = make(map[int]string)


m[1] = "apple" // OK

}

3. Проверка наличия ключа

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

Карты и Go Golang, Программирование, Карты, Длиннопост

package main

import (

"fmt"

)

func main() {

m := make(map[int]int)

k := 123

val := m[1] // val == 0, ключ 123 не существует, но 0 также может быть допустимым значением

fmt.Println(val)

// Лучше использовать двухзначную форму получения элемента:

if _, ok := m[k]; !ok {

fmt.Printf("map key: %d, not found\n", k)

}

}

4. Сортировка

В Go карты не имеют внутреннего порядка, и их элементы хранятся в случайном порядке. Однако вы можете отсортировать ключи (или значения) карты, используя дополнительные шаги. Вот как можно отсортировать карту по ключам:

Карты и Go Golang, Программирование, Карты, Длиннопост

package main

import (

"fmt"

"sort"

)

func main() {

m := map[string]int{

"banana": 3,

"apple":  5,

"pear":  2,

"orange": 4,

}

// Создаем слайс для хранения ключей

var keys []string

// Добавляем ключи в слайс

for k := range m {

keys = append(keys, k)

}

// Сортируем ключи

sort.Strings(keys)


// Выводим отсортированную карту

for _, k := range keys {

fmt.Println(k, m[k])

}

}

5. Конкурентная модификация

Карты в Go не являются потокобезопасными, и одновременная модификация карты из разных горутин может привести к состоянию гонки и панике.

Карты и Go Golang, Программирование, Карты, Длиннопост

package main

import "time"

func main() {

m := make(map[int]int)

// Имитация конкурентной модификации

go func() {

for i := 0; i < 10000; i++ {

m[i] = i

}

}()

go func() {

for i := 0; i < 10000; i++ {

delete(m, i)

}

}()

go func() {

for i := 0; i < 10000; i++ {

delete(m, i)

}

}()

// Возможна паника или другое непредсказуемое поведение

time.Sleep(time.Second * 100)

}

Решение.

Для предотвращения конкурентной модификации карты и обеспечения потокобезопасности, можно использовать мьютекс из пакета sync. Мьютекс позволяет блокировать доступ к ресурсу (в данном случае к карте) для всех горутин, кроме одной, которая в данный момент владеет мьютексом. Это гарантирует, что только одна горутина может изменять карту в любой момент времени.

Пример решения с использованием мьютекса:

Карты и Go Golang, Программирование, Карты, Длиннопост

package main

import (

"sync"

"time"

)

func main() {

var m = make(map[int]int) // Создаем карту

var mutex sync.Mutex // Создаем мьютекс

// Горутина для добавления элементов в карту

go func() {

for i := 0; i < 10000; i++ {

mutex.Lock() // Блокируем мьютекс перед модификацией карты

m[i] = i

mutex.Unlock() // Разблокируем мьютекс после модификации карты

}

}()

// Горутина для удаления элементов из карты

go func() {

for i := 0; i < 10000; i++ {

mutex.Lock() // Блокируем мьютекс перед модификацией карты

delete(m, i)

mutex.Unlock() // Разблокируем мьютекс после модификации карты

}

}()

// Даем горутинам время на выполнение

time.Sleep(time.Second)

}

В данном примере перед каждым изменением карты мьютекс блокируется вызовом mutex.Lock(), и разблокируется вызовом mutex.Unlock() после изменения. Это гарантирует, что в любой момент времени карта может быть изменена только одной горутиной, предотвращая конкурентную модификацию и потенциальные ошибки в программе.

Использование каналов: Каналы в Go могут использоваться не только для обмена данными между горутинами, но и как средство синхронизации. Вы можете создать канал, через который будут передаваться операции чтения и записи карты, а одна горутина будет отвечать за выполнение этих операций. Это гарантирует, что все операции с картой выполняются последовательно.

Карты и Go Golang, Программирование, Карты, Длиннопост

package main

import (

"sync"

)

type command struct {

key  int

value int

op  string // "set", "delete", "get"

}

func main() {

var opChan = make(chan command)

var wg sync.WaitGroup

// Горутина для управления картой

go func() {

m := make(map[int]int)

for op := range opChan {

switch op.op {

case "set":

m[op.key] = op.value

case "delete":

delete(m, op.key)

}

}

}()

// Горутина для добавления элементов

wg.Add(1)

go func() {

defer wg.Done()

for i := 0; i < 100; i++ {

opChan <- command{key: i, value: i, op: "set"}

}

}()

// Горутина для удаления элементов

wg.Add(1)

go func() {

defer wg.Done()

for i := 0; i < 100; i++ {

opChan <- command{key: i, op: "delete"}

}

}()

wg.Wait()

close(opChan)

}

sync.Map: В пакете sync есть специальная структура Map, предназначенная для использования в многопоточных средах без явного использования мьютексов для синхронизации. sync.Map имеет встроенные методы для безопасной работы с данными в конкурентных ситуациях.

Карты и Go Golang, Программирование, Карты, Длиннопост

package main

import (

"fmt"

"sync"

)

func main() {

var m sync.Map

// Установка значения

m.Store(1, "a")

// Получение значения

if val, ok := m.Load(1); ok {

fmt.Println("Value:", val)

}

// Удаление значения

m.Delete(1)

}

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

Показать полностью 10
[моё] Golang Программирование Карты Длиннопост
4
FreshAngry007
1 год назад

Массивы и go⁠⁠

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

Основные характеристики массивов в Go:

  • Фиксированный размер: Длина массива определяется при его объявлении и не может быть изменена. Это означает, что добавление элементов в массив или удаление из него невозможно без создания нового массива.

  • Однородность: Все элементы массива должны быть одного и того же типа.

  • Инициализация: Массивы могут быть инициализированы явно при объявлении или позже в коде. Также Go поддерживает инициализацию массива с помощью синтаксиса литералов.

  • Производительность: Доступ к элементам массива происходит очень быстро, так как элементы хранятся в непрерывной области памяти.

Объявление и инициализация массива:

var arr [5]int // Объявление массива из 5 целых чисел, инициализированного нулями

arr2 := [3]int{1, 2, 3} // Объявление и инициализация массива с тремя элементами

arr3 := [...]int{1, 2, 3, 4, 5} // Использование ... позволяет компилятору определить длину массива

Работа с элементами массива:

arr[0] = 10 // Присвоение значения первому элементу массива

fmt.Println(arr[1]) // Доступ к элементу массива

Передача массивов в функции:

Важно помнить, что массивы в Go передаются в функции по значению, что означает создание их копии. Для больших массивов это может быть неэффективно по памяти и времени. В таких случаях лучше использовать слайсы, которые являются ссылками на массивы.

func printArray(arr [3]int) {

for _, value := range arr {

fmt.Println(value)

}

}

myArray := [3]int{10, 20, 30}

printArray(myArray) // Передача массива в функцию

Использование массивов в Go имеет несколько нюансов и потенциальных "подводных камней", на которые стоит обратить внимание:

1. Фиксированный размер

Основное ограничение массивов заключается в их фиксированном размере. Размер массива является частью его типа, что означает, что вы должны знать размер массива на этапе компиляции. Это может быть неудобно в ситуациях, когда размер данных заранее неизвестен или может изменяться во время выполнения программы. В таких случаях лучше использовать слайсы, которые являются более гибкой альтернативой массивам.

2. Передача массива функции по значению

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

3. Инициализация массивов неявными значениями

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

4. Ограниченная поддержка встроенных операций

В отличие от слайсов, для массивов в Go доступно меньше встроенных операций. Например, нельзя использовать функцию append для добавления элементов в массив, поскольку это изменит его размер, что невозможно для массивов.

5. Неудобство в использовании

Из-за строгих ограничений на размер и тип, работа с массивами может быть менее удобной, чем с слайсами, особенно когда требуется динамическое изменение размера коллекции или её частая передача между функциями.

Решения

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

Показать полностью
[моё] Программирование Golang IT Текст Массивы
0
FreshAngry007
1 год назад

Слайсы и Go⁠⁠

Слайсы в Go — это удобный, динамический интерфейс к массивам. Они обеспечивают большую гибкость, по сравнению с традиционными массивами. В отличие от массивов, размер слайса не является частью его типа, что позволяет слайсам быть гораздо более динамичными.

Слайс состоит из трех компонентов:

  • Указатель на начальный элемент массива, к которому применяется слайс.

  • Длина слайса (len), указывающая на количество элементов в слайсе.

  • Емкость слайса (cap), которая указывает на максимальное количество элементов, начиная с текущего указателя, до конца базового массива.

Как слайсы работают

Когда вы создаете слайс, Go автоматически выделяет под него массив в памяти. Слайс предоставляет ссылку на начало этого массива и позволяет работать с его частью. Изменение элементов слайса напрямую влияет на соответствующие элементы массива, так как слайс является просто "окном" в массив.

Динамическое изменение размера

Одной из ключевых особенностей слайсов является их способность динамически изменять размер. Функция append позволяет добавлять элементы в слайс. Если при добавлении элемента емкость слайса недостаточна для размещения нового элемента, Go автоматически выделяет новый, больший массив и копирует в него элементы из старого. В результате, слайс начинает ссылаться на новый массив.

Создание и инициализация

Слайсы можно создать несколькими способами:

  • Используя оператор make, который позволяет указать тип, начальную длину и (необязательно) емкость слайса. Например, make([]int, 5, 10).

  • Через литерал слайса, например, []int{1, 2, 3}.

  • Оператором [:], [low:high], [low:high:max] для создания слайса из массива или другого слайса.

Подводные камни

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

Пара примеров с подводными камнями при работе со слайсами в Go:

1. Непреднамеренное изменение базового массива

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

package main

import "fmt"

func modifySlice(s []int) {

s[0] = 999 // Изменяет первый элемент базового массива

}

func main() {

originalSlice := []int{1, 2, 3}

modifySlice(originalSlice)

// Вывод: [999 2 3]

fmt.Println(originalSlice)

}

В этом примере, изменяя слайс внутри функции modifySlice, мы также изменяем исходный слайс originalSlice, потому что оба они ссылаются на один и тот же базовый массив.

1.1. Решение

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

package main

import "fmt"

func CopyAndModifySlice(s []int) []int {

// Создаем новый слайс и копируем в него данные из s

newSlice := make([]int, len(s))

copy(newSlice, s)

// Изменяем новый слайс

newSlice[0] = 999

return newSlice

}

func main() {

originalSlice := []int{1, 2, 3}

modifynewSlice := CopyAndModifySlice(originalSlice)

// Вывод: [1 2 3]

fmt.Println(originalSlice)

// Вывод: [999 2 3]

fmt.Println(modifynewSlice )

}

2. Утечка памяти из-за неправильного срезания слайсов

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

package main

import "fmt"

func getSmallSlice() []int {

// Большой массив

bigSlice := make([]int, 1000000)

// Маленький слайс, ссылается на начало большого массива

smallSlice := bigSlice[:3]

return smallSlice

}

func main() {

smallSlice := getSmallSlice()

fmt.Println(cap(smallSlice)) // Вывод: 1000000

// В этот момент вся память, занимаемая bigSlice, все еще занята,

// несмотря на то что мы работаем только с маленьким сегментом.

}

В этом примере, bigSlice больше недоступен после выполнения getSmallSlice, но память, которую он занимает, остается занятой из-за smallSlice, который ссылается на часть bigSlice. Это может привести к неэффективному использованию памяти, особенно если подобный код выполняется многократно.

2.1. Решение

package main

import "fmt"

func getSmallSlice() []int {

// Большой массив

bigSlice := make([]int, 1000000)

// Создаем новый слайс того же размера, что и нужный маленький слайс

smallSlice := make([]int, 3)

// Копируем данные из большого массива в маленький слайс

copy(smallSlice, bigSlice[:3])

return smallSlice

}

func main() {

smallSlice := getSmallSlice()

fmt.Println(cap(smallSlice)) // Вывод: 3

// Теперь большой массив может быть собран сборщиком мусора,

// так как на него нет ссылок в виде слайса.

}

3. Увеличение емкости (capacity) слайса неявно

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

package main

import "fmt"

func main() {

// Слайс с начальной емкостью 1

s := make([]int, 0, 1)

prevCap := cap(s)

// Потенциально много раз увеличиваем емкость

for i := 0; i < 1024; i++ {

if cap(s) != prevCap {

fmt.Printf("Емкость изменилась с %d на %d\n", prevCap, cap(s))

prevCap = cap(s)

}

s = append(s, i)

}

}

// Емкость изменилась с 1 на 2

// Емкость изменилась с 2 на 4

// Емкость изменилась с 4 на 8

// Емкость изменилась с 8 на 16

// Емкость изменилась с 16 на 32

// Емкость изменилась с 32 на 64

// Емкость изменилась с 64 на 128

// Емкость изменилась с 128 на 256

// Емкость изменилась с 256 на 512

// Емкость изменилась с 512 на 848

// Емкость изменилась с 848 на 1280

В этом примере слайс s начинается с емкости 1 и увеличивается многократно по мере добавления элементов, что может привести к множественным аллокациям памяти и копированиям.

3.1. Решение

package main

import "fmt"

func main() {

// Заранее выделяем слайс с нужной емкостью

// Емкость сразу выделена под 1024 элемента

s := make([]int, 0, 1024)

// Добавляем элементы, не вызывая дополнительных аллокаций

for i := 0; i < 1024; i++ {

s = append(s, i)

}

// Вывод: 1024 1024

fmt.Println(len(s), cap(s))

}

// емкость не менялась не разу

4. Потенциальная потеря данных при неосторожном использовании append

Если append используется с исходным слайсом, который имеет недостаточную емкость для добавления новых элементов, будет создан новый слайс с увеличенной емкостью, и исходный слайс не будет изменен.

package main

import "fmt"

func main() {

s1 := make([]int, 0, 5)

// s2 указывает на новый слайс

s2 := append(s1, 1)

// s3 тоже указывает на новый слайс, но уже другой

s3 := append(s1, 2)

// [] [1] [2] - s1 остается без изменений

fmt.Println(s1, s2, s3)

}

Это поведение может привести к неожиданным результатам, если предполагается, что append изменит исходный слайс.

4.1. Решение

Чтобы избежать потенциальной потери данных при использовании append, особенно когда вы работаете с исходным слайсом, который может не иметь достаточной емкости для добавления новых элементов, важно убедиться, что вы корректно обрабатываете возвращаемое значение от append. append возвращает новый слайс, который может указывать на тот же или новый базовый массив в зависимости от емкости исходного слайса.

Если ваш код полагается на изменение исходного слайса, вы должны всегда присваивать результат append обратно исходному слайсу:

package main

import "fmt"

func main() {

s1 := make([]int, 0, 5)

// Присваиваем результат append обратно s1

s1 = append(s1, 1)

// s2 теперь будет содержать [1 2]

s2 := append(s1, 2)

// Вывод: [1] [1 2] - s1 изменен корректно

fmt.Println(s1, s2)

}

Поиграться с примерами можно тут https://go.dev/play

Показать полностью
[моё] Программирование Golang IT Длиннопост Текст Слайсы
0
2
PENTEST.DNA
PENTEST.DNA
1 год назад
Web-технологии

Создание TCP-прокси⁠⁠

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Вы можете реализовать все 𝗧𝗖𝗣-взаимодействия, используя встроенный в 𝗚𝗼 пакет 𝗻𝗲𝘁. В предыдущем разделе мы сосредоточились главным образом на его применении с позиции клиента. В этом же разделе задействуем его для создания 𝗧𝗖𝗣-серверов и передачи данных. Изучение этого процесса начнется с создания эхо-сервера — сервера, который просто возвращает запрос обратно клиенту. Затем мы создадим две более универсальные в применении программы: переадресатор 𝗧𝗖𝗣-портов и 𝗡𝗲𝘁𝗰𝗮𝘁-функцию «зияющая дыра в безопасности», применяемую для удаленного выполнения команд.


Использование io.Reader и io.Writer



При создании примеров этого раздела вам потребуется задействовать два значимых типа: 𝗶𝗼.𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗶𝗼.𝗪𝗿𝗶𝘁𝗲𝗿. Они необходимы для всех задач ввода/вывода (𝗜/𝗢) вне зависимости от того, задействуете вы 𝗧𝗖𝗣, 𝗛𝗧𝗧𝗣, файловую систему или любые другие средства. Будучи частью встроенного в 𝗚𝗼 пакета 𝗶𝗼, эти типы являются краеугольным камнем любой передачи данных, как локальной, так и сетевой. В документации они определены так:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Оба типа определяются как интерфейсы, то есть напрямую их создать нельзя. Каждый тип содержит определение одной экспортируемой функции: 𝗥𝗲𝗮𝗱 или 𝗪𝗿𝗶𝘁𝗲. Можно рассматривать эти функции как абстрактные методы, которые должны быть реализованы в типе, чтобы он считался 𝗥𝗲𝗮𝗱𝗲𝗿 или 𝗪𝗿𝗶𝘁𝗲𝗿. Например, следующий искусственный тип выполняет это соглашение и может использоваться там, где приемлем 𝗥𝗲𝗮𝗱𝗲𝗿:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Давайте с помощью них создадим что-нибудь полуготовое: настраиваемый 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿, обертывающий 𝘀𝘁𝗱𝗶𝗻 и 𝘀𝘁𝗱𝗼𝘂𝘁. Код для этого тоже будет несколько искусственным, так как типы 𝗚𝗼 𝗼𝘀.𝗦𝘁𝗱𝗶𝗻 и 𝗼𝘀.𝗦𝘁𝗱𝗼𝘂𝘁 уже действуют как 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿. Но если не пытаться изобрести колесо, то ничему и не научишься, ведь так?

Ниже показана полная реализация, а далее дано пояснение.

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Реализация reader и writer /io-example/main.go

Этот код определяет два пользовательских типа: 𝗙𝗼𝗼𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗙𝗼𝗼𝗪𝗿𝗶𝘁𝗲𝗿. В каждом типе вы определяете конкретную реализацию функции 𝗥𝗲𝗮𝗱([]𝗯𝘆𝘁𝗲) для 𝗙𝗼𝗼𝗥𝗲𝗮𝗱𝗲𝗿 и функции 𝗪𝗿𝗶𝘁𝗲([]𝗯𝘆𝘁𝗲) для 𝗙𝗼𝗼𝗪𝗿𝗶𝘁𝗲𝗿. В этом случае обе функции считывают из 𝘀𝘁𝗱𝗶𝗻 и записывают в 𝘀𝘁𝗱𝗼𝘂𝘁.

Обратите внимание на то, что функции 𝗥𝗲𝗮𝗱 и в 𝗙𝗼𝗼𝗥𝗲𝗮𝗱𝗲𝗿, и в 𝗼𝘀.𝗦𝘁𝗱𝗶𝗻 возвращают длину данных и все ошибки. Сами эти данные копируются в срез 𝗯𝘆𝘁𝗲, передаваемый этой функции. Это согласуется с начальным определением интерфейса 𝗥𝗲𝗮𝗱𝗲𝗿, приведенным в данном разделе ранее. Функция 𝗺𝗮𝗶𝗻() создает этот срез с названием 𝗶𝗻𝗽𝘂𝘁 и затем использует его в вызовах к 𝗙𝗼𝗼𝗥𝗲𝗮𝗱𝗲𝗿.𝗥𝗲𝗮𝗱([]𝗯𝘆𝘁𝗲) и 𝗙𝗼𝗼𝗥𝗲𝗮𝗱𝗲𝗿.𝗪𝗿𝗶𝘁𝗲([]𝗯𝘆𝘁𝗲).

При пробном запуске программы мы получим следующий вывод:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Копирование данных из 𝗥𝗲𝗮𝗱𝗲𝗿 в 𝗪𝗿𝗶𝘁𝗲𝗿 — это настолько распространенный шаблон, что пакет 𝗶𝗼 содержит специальную функцию 𝗖𝗼𝗽𝘆(), которую можно задействовать для упрощения функции 𝗺𝗮𝗶𝗻(). Вот ее прототип:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Эта удобная функция позволяет реализовывать то же поведение программы, что и ранее, заменив 𝗺𝗮𝗶𝗻() кодом, показанным ниже.

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Применение io.Copy /ch-2/copy-example/main.go

Обратите внимание, что явные вызовы 𝗿𝗲𝗮𝗱𝗲𝗿.𝗥𝗲𝗮𝗱([ ]𝗯𝘆𝘁𝗲) и 𝘄𝗿𝗶𝘁𝗲𝗿.𝗪𝗿𝗶𝘁𝗲([ ] 𝗯𝘆𝘁𝗲) были замещены одним вызовом 𝗶𝗼.𝗖𝗼𝗽𝘆(𝘄𝗿𝗶𝘁𝗲𝗿, 𝗿𝗲𝗮𝗱𝗲𝗿). Внутренне 𝗶𝗼.𝗖𝗼𝗽𝘆(𝘄𝗿𝗶𝘁𝗲𝗿, 𝗿𝗲𝗮𝗱𝗲𝗿) вызывает в переданном ридере функцию 𝗥𝗲𝗮𝗱([ ]𝗯𝘆𝘁𝗲), в результате чего 𝗙𝗼𝗼𝗥𝗲𝗮𝗱𝗲𝗿 выполняет считывание из 𝘀𝘁𝗱𝗶𝗻. Далее 𝗶𝗼.𝗖𝗼𝗽𝘆(𝘄𝗿𝗶𝘁𝗲𝗿, 𝗿𝗲𝗮𝗱𝗲𝗿) вызывает в переданном райтере функцию 𝗪𝗿𝗶𝘁𝗲([ ]𝗯𝘆𝘁𝗲), что приводит к вызову 𝗙𝗼𝗼𝗪𝗿𝗶𝘁𝗲𝗿, записывающего данные в 𝘀𝘁𝗱𝗼𝘂𝘁. По сути, 𝗶𝗼.𝗖𝗼𝗽𝘆(𝘄𝗿𝗶𝘁𝗲𝗿, 𝗿𝗲𝗮𝗱𝗲𝗿) обрабатывает последовательный процесс чтения/записи без лишних деталей.

Этот вводный раздел никак нельзя считать подробным рассмотрением системы 𝗜/𝗢 и интерфейсов в 𝗚𝗼. Многие вспомогательные функции и пользовательские ридеры/райтеры существуют как часть стандартных пакетов 𝗚𝗼. В большинстве случаев эти стандартные пакеты содержат все основные реализации, необходимые для реализации большинства распространенных задач. В следующем разделе мы рассмотрим применение всех этих основ к 𝗧𝗖𝗣-коммуникациям и в итоге применим полученные навыки для разработки реальных рабочих инструментов.



Создание эхо-сервера:

Как и во многих языках, изучение процесса чтения/записи данных с сокета мы начнем с построения эхо-сервера. Для этого будем использовать 𝗻𝗲𝘁.𝗖𝗼𝗻𝗻 — потокоориентированное соединение 𝗚𝗼, с которым вы уже познакомились при создании сканера портов. Как указано в документации для этого типа данных, 𝗖𝗼𝗻𝗻 реализует функции 𝗥𝗲𝗮𝗱([ ]𝗯𝘆𝘁𝗲) и 𝗪𝗿𝗶𝘁𝗲([ ]𝗯𝘆𝘁𝗲) согласно определению для интерфейсов 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿. Следовательно, 𝗖𝗼𝗻𝗻 одновременно является и 𝗥𝗲𝗮𝗱𝗲𝗿, и 𝗪𝗿𝗶𝘁𝗲𝗿 (да, такое возможно). Это вполне логично, так как 𝗧𝗖𝗣-соединения двунаправленные и могут использоваться для отправки (записи) и получения (чтения) данных.

После создания экземпляра conn вы сможете отправлять и получать данные через TCP-сокет. Тем не менее TCP-сервер не может просто создать соединение, его должен установить клиент. В 𝗚𝗼 для начального открытия 𝗧𝗖𝗣-слушателя на конкретном порте можно использовать 𝗻𝗲𝘁.𝗟𝗶𝘀𝘁𝗲𝗻(𝗻𝗲𝘁𝘄𝗼𝗿𝗸, 𝗮𝗱𝗱𝗿𝗲𝘀𝘀 𝘀𝘁𝗿𝗶𝗻𝗴). После подключения клиента метод 𝗔𝗰𝗰𝗲𝗽𝘁() создает и возвращает объект 𝗖𝗼𝗻𝗻, который вы можете применять для получения и отправки данных.

Ниже показан полноценный пример реализации сервера. Для большей ясности мы добавили в код комментарии. Не стремитесь сразу понять весь код, так как позже мы подробно его поясним.

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Базовый эхо-сервер /ch-2/echo-server/main.go

Базовый эхо-сервер начинается с определения функции 𝗲𝗰𝗵𝗼(𝗻𝗲𝘁.𝗖𝗼𝗻𝗻), которая принимает в качестве параметра экземпляр 𝗖𝗼𝗻𝗻. Он выступает в роли обработчика соединения, выполняя все необходимые операции 𝗜/𝗢. Эта функция повторяется бесконечно, используя буфер для считывания данных из соединения и их записи в него. Данные считываются в переменную 𝗯, после чего записываются обратно в соединение.

Теперь нужно настроить слушатель, который будет вызывать обработчик. Как ранее говорилось, сервер не может сам создать соединение и должен прослушивать подключение клиента. Следовательно, слушатель, определенный как 𝘁𝗰𝗽, привязанный к порту 𝟮𝟬𝟬𝟴𝟬, запускается во всех интерфейсах посредством функции 𝗻𝗲𝘁.𝗟𝗶𝘀𝘁𝗲𝗻(𝗻𝗲𝘁𝘄𝗼𝗿𝗸, 𝗮𝗱𝗱𝗿𝗲𝘀𝘀 𝘀𝘁𝗿𝗶𝗻𝗴).

Далее бесконечный цикл обеспечивает, чтобы сервер продолжал прослушивание соединений даже после того, как оно было установлено. В этом цикле происходит вызов 𝗹𝗶𝘀𝘁𝗲𝗻𝗲𝗿.𝗔𝗰𝗰𝗲𝗽𝘁() ❻ — функции, блокирующей выполнение при ожидании подключений. Когда клиент подключается, эта функция возвращает экземпляр 𝗖𝗼𝗻𝗻. Напомним, что 𝗖𝗼𝗻𝗻 является и 𝗥𝗲𝗮𝗱𝗲𝗿, и 𝗪𝗿𝗶𝘁𝗲𝗿 (реализует методы интерфейса 𝗥𝗲𝗮𝗱([ ]𝗯𝘆𝘁𝗲) и 𝗪𝗿𝗶𝘁𝗲([ ]𝗯𝘆𝘁𝗲)).

После этого экземпляр 𝗖𝗼𝗻𝗻 передается в функцию обработки 𝗲𝗰𝗵𝗼(𝗻𝗲𝘁.𝗖𝗼𝗻𝗻). Перед ее вызовом указано ключевое слово 𝗴𝗼, делающее этот вызов многопоточным, в результате чего другие подключения в ожидании завершения функции-обработчика не блокируются. Это может показаться излишним для столь простого сервера, но мы добавили эту функциональность для демонстрации простоты паттерна многопоточности 𝗚𝗼 на случай, если вы еще не до конца его поняли. В данный момент у вас есть два легковесных параллельно выполняющихся потока.

  • Основной поток зацикливается и блокируется функцией listener.Accept() на время ожидания ею следующего подключения.

  • Горутина обработки, чье выполнение было передано в функцию echo(net.Conn), возобновляется и обрабатывает данные.


Далее показан пример использования 𝗧𝗲𝗹𝗻𝗲𝘁 в качестве подключающегося клиента:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Сервер производит следующий стандартный вывод:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост


Революционно, не правда ли? Сервер, возвращающий клиенту в точности то, что клиент ему отправил. Очень полезный и сильный пример!



Создание буферизованного слушателя для улучшения кода:



Пример в коде Базового эхо-сервера работает прекрасно, но он опирается на чисто низкоуровневые вызовы функции, отслеживание буфера и повторяющиеся циклы чтения/записи. Это довольно утомительный и подверженный ошибкам процесс. К счастью, в 𝗚𝗼 есть и другие пакеты, которые могут его упростить и уменьшить сложность кода. Говоря конкретнее, пакет 𝗯𝘂𝗳𝗶𝗼 обертывает 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿 для создания буферизованного механизма 𝗜/𝗢. Далее приведена обновленная функция 𝗲𝗰𝗵𝗼(𝗻𝗲𝘁.𝗖𝗼𝗻𝗻) с сопутствующим описанием изменений:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

В экземпляре 𝗖𝗼𝗻𝗻 больше не происходит прямого вызова функций 𝗥𝗲𝗮𝗱([]𝗯𝘆𝘁𝗲) и 𝗪𝗿𝗶𝘁𝗲([]𝗯𝘆𝘁𝗲). Вместо этого вы инициализируете новые буферизованные 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿 через 𝗡𝗲𝘄𝗥𝗲𝗮𝗱𝗲𝗿(𝗶𝗼.𝗥𝗲𝗮𝗱𝗲𝗿) и 𝗡𝗲𝘄𝗪𝗿𝗶𝘁𝗲𝗿(𝗶𝗼.𝗪𝗿𝗶𝘁𝗲𝗿). Оба вызова в качестве параметра получают существующие 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿 (помните, что тип 𝗖𝗼𝗻𝗻 реализует необходимые функции, чтобы считаться 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿).

Оба буферизованных экземпляра содержат вспомогательные функции для чтения и сохранения данных. 𝗥𝗲𝗮𝗱𝗦𝘁𝗿𝗶𝗻𝗴(𝗯𝘆𝘁𝗲) получает символ-разграничитель, обозначая, до какой точки выполнять считывание, а 𝗪𝗿𝗶𝘁𝗲𝗦𝘁𝗿𝗶𝗻𝗴(𝗯𝘆𝘁𝗲) записывает строку в сокет. При записи данных вам нужно явно вызывать 𝘄𝗿𝗶𝘁𝗲𝗿.𝗙𝗹𝘂𝘀𝗵() для сброса всех данных внутреннему райтеру (в данном случае экземпляру 𝗖𝗼𝗻𝗻).

Несмотря на то что предыдущий пример упрощает процесс, применяя буферизацию 𝗜/𝗢, вы можете переработать его под использование вспомогательной функции 𝗖𝗼𝗽𝘆(𝗪𝗿𝗶𝘁𝗲𝗿, 𝗥𝗲𝗮𝗱𝗲𝗿). Напомним, что функция получает в качестве ввода целевой 𝗪𝗿𝗶𝘁𝗲𝗿 и исходный 𝗥𝗲𝗮𝗱𝗲𝗿, просто выполняя копирование из источника в место назначения.

В этом примере вы передаете переменную 𝗰𝗼𝗻𝗻 и как источник, и как место назначения, так как в итоге будете отражать содержимое обратно в установленное соединение:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Вот вы и познакомились с основами системы 𝗜/𝗢, попутно применив ее к 𝗧𝗖𝗣-серверам. Пришло время перейти к более полезным и представляющим для вас интерес примерам.



Проксирование TCP-клиента:



Теперь, когда у вас под ногами есть твердая почва, можете применить полученные навыки для создания простого переадресатора портов для проксирования соединения через промежуточный сервис или хост. Как уже говорилось, это пригождается для обхода ограничивающего контроля исходящего трафика или использования системы с целью обхода сегментации сети. Прежде чем перейти к коду, рассмотрите вымышленную, но вполне реалистичную задачу: Андрей является малоэффективным сотрудником компании 𝗔𝗖𝗠𝗘 𝗜𝗻𝗰., работая на должности бизнес-аналитика и получая приличную зарплату просто потому, что слегка приукрасил данные своего резюме. (Неужели он реально учился в школе Лиги плюща? Андрей, такой обман неэтичен.) Недостаток мотивации Андрея может по силе сравниться разве что с его любовью к кошкам — такой сильной, что он даже установил дома специальные видеокамеры и создал сайт 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲, через который удаленно следил за своими мохнатыми питомцами. Тем не менее здесь была одна сложность: 𝗔𝗖𝗠𝗘 следит за Андреем. Им не нравится, что он круглые сутки передает потоковое видео своих кошек в ультравысоком разрешении 𝟰𝗞, занимая ценный пропускной канал сети. Компания даже заблокировала своим сотрудникам возможность посещать его кошачий сайт.

Но у хитрого Андрея и здесь возник план: «А что, если я настрою переадресатор портов в подконтрольной мне интернет-системе и буду перенаправлять весь трафик с этого хоста на 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲?» На следующий день Андрей отмечается на работе и убеждается в возможности доступа к личному сайту, размещенному на домене 𝗷𝗼𝗲𝘀𝗽𝗿𝗼𝘅𝘆.𝗰𝗼𝗺. Он пропускает все встречи после обеда и отправляется в кафетерий, где быстро пишет код для своей задачи, подразумевающей перенаправление на 𝗵𝘁𝘁𝗽://𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲 всего входящего на 𝗵𝘁𝘁𝗽://𝗷𝗼𝗲𝘀𝗽𝗿𝗼𝘅𝘆.𝗰𝗼𝗺 трафика.

Вот код Андрея, который он запускает на сервере 𝗷𝗼𝗲𝘀𝗽𝗿𝗼𝘅𝘆.𝗰𝗼𝗺:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Начнем с рассмотрения функции 𝗵𝗮𝗻𝗱𝗹𝗲(𝗻𝗲𝘁.𝗖𝗼𝗻𝗻). Андрей подключается к 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲 (вспомните, что этот хост недоступен напрямую с его рабочего места). Затем он использует 𝗖𝗼𝗽𝘆(𝗪𝗿𝗶𝘁𝗲𝗿, 𝗥𝗲𝗮𝗱𝗲𝗿) в двух разных местах. Первый экземпляр обеспечивает копирование данных из входящего соединения в соединение 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲. Второй же обеспечивает, чтобы считанные из 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲 данные записывались обратно в соединение подключающегося клиента. Так как 𝗖𝗼𝗽𝘆(𝗪𝗿𝗶𝘁𝗲𝗿, 𝗥𝗲𝗮𝗱𝗲𝗿) является блокирующей функцией и будет продолжать блокировать выполнение, пока сетевое соединение открыто, Андрей предусмотрительно обертывает первый вызов 𝗖𝗼𝗽𝘆(𝗪𝗿𝗶𝘁𝗲𝗿, 𝗥𝗲𝗮𝗱𝗲𝗿) в новую горутину. Это гарантирует продолжение выполнения в функции 𝗵𝗮𝗻𝗱𝗹𝗲(𝗻𝗲𝘁.𝗖𝗼𝗻𝗻) и дает возможность выполнить второй вызов 𝗖𝗼𝗽𝘆(𝗪𝗿𝗶𝘁𝗲𝗿, 𝗥𝗲𝗮𝗱𝗲𝗿).

Прокси-сервер Андрей прослушивает порт 𝟴𝟬 и ретранслирует весь трафик, получаемый через это соединение, на порт 𝟴𝟬 сайта 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲 и обратно. Этот безумный и расточительный парень убеждается, что может подключаться к 𝗷𝗼𝗲𝘀𝗰𝗮𝘁𝗰𝗮𝗺.𝘄𝗲𝗯𝘀𝗶𝘁𝗲 через 𝗷𝗼𝗲𝘀𝗽𝗿𝗼𝘅𝘆.𝗰𝗼𝗺 с помощью 𝗰𝘂𝗿𝗹:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Так Андрей успешно реализует коварный замысел. Он прекрасно устроился, получив возможность в оплачиваемое 𝗔𝗖𝗠𝗘 время использовать их же канал связи для наблюдения за жизнью своих питомцев.



Воспроизведение функции Netcat для выполнения команд:



В этом разделе мы воспроизведем одну из наиболее интересных функций 𝗡𝗲𝘁𝗰𝗮𝘁 — «зияющую дыру в безопасности».

𝗡𝗲𝘁𝗰𝗮𝘁 — это как швейцарский армейский нож для 𝗧𝗖𝗣/𝗜𝗣, который представляет собой более гибкую версию 𝗧𝗲𝗹𝗻𝗲𝘁 с поддержкой сценариев. Эта утилита имеет возможность перенаправлять 𝘀𝘁𝗱𝗶𝗻 и 𝘀𝘁𝗱𝗼𝘂𝘁 любой произвольной программы через 𝗧𝗖𝗣, позволяя атакующему, например, превратить уязвимость к выполнению одной команды в доступ к оболочке операционной системы. Взгляните:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Эта команда создает прослушивающий сервер на порте 𝟭𝟯𝟯𝟯𝟳. Любой подключающийся, возможно, через 𝗧𝗲𝗹𝗻𝗲𝘁, клиент сможет выполнить любые команды 𝗯𝗮𝘀𝗵 — вот почему данную функцию и называют зияющей дырой в безопасности. 𝗡𝗲𝘁𝗰𝗮𝘁 позволяет при желании включить такую возможность в процессе компиляции программы. (По понятным причинам большинство исполняемых файлов 𝗡𝗲𝘁𝗰𝗮𝘁 в стандартных сборках 𝗟𝗶𝗻𝘂𝘅 ее не включают.) Эта функция настолько потенциально опасна, что мы покажем, как воссоздать ее в 𝗚𝗼.

Для начала загляните в пакет 𝗚𝗼 𝗼𝘀/𝗲𝘅𝗲𝗰. Он будет использоваться для выполнения команд операционной системы. Этот пакет определяет тип 𝗖𝗺𝗱, который содержит необходимые методы и свойства для выполнения команд и управления 𝘀𝘁𝗱𝗶𝗻 и 𝘀𝘁𝗱𝗼𝘂𝘁. Вы будете перенаправлять 𝘀𝘁𝗱𝗶𝗻 (𝗥𝗲𝗮𝗱𝗲𝗿) и 𝘀𝘁𝗱𝗼𝘂𝘁 (𝗪𝗿𝗶𝘁𝗲𝗿) в экземпляр 𝗖𝗼𝗻𝗻, представляющий и 𝗥𝗲𝗮𝗱𝗲𝗿, и 𝗪𝗿𝗶𝘁𝗲𝗿.

При получении нового подключения создать экземпляр 𝗖𝗺𝗱 можно с помощью функции 𝗖𝗼𝗺𝗺𝗮𝗻𝗱(𝗻𝗮𝗺𝗲 𝘀𝘁𝗿𝗶𝗻𝗴, 𝗮𝗿𝗴 ...𝘀𝘁𝗿𝗶𝗻𝗴) из 𝗼𝘀/𝗲𝘅𝗲𝗰. Эта функция получает в качестве параметров команды ОС и любые аргументы. В данном примере нужно жестко закодировать в качестве команды /𝗯𝗶𝗻/𝘀𝗵 и передать в качестве аргумента -𝗶, чтобы перейти в интерактивный режим, из которого можно будет управлять потоками 𝘀𝘁𝗱𝗶𝗻 и 𝘀𝘁𝗱𝗼𝘂𝘁 более уверенно:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Эта инструкция создает экземпляр 𝗖𝗺𝗱, но команду еще не выполняет. Здесь для управления 𝘀𝘁𝗱𝗶𝗻 и 𝘀𝘁𝗱𝗼𝘂𝘁 есть два варианта: использовать 𝗖𝗼𝗽𝘆(𝗪𝗿𝗶𝘁𝗲𝗿, 𝗥𝗲𝗮𝗱𝗲𝗿), как говорилось ранее, или напрямую присвоить 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿 экземпляру 𝗖𝗺𝗱. Давайте непосредственно присвоим объект 𝗖𝗼𝗻𝗻 экземплярам 𝗰𝗺𝗱.𝗦𝘁𝗱𝗶𝗻 и 𝗰𝗺𝗱.𝗦𝘁𝗱𝗼𝘂𝘁:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

После настройки команды и потоков запустить ее можно с помощью 𝗰𝗺𝗱.𝗥𝘂𝗻():

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Такая логика прекрасно работает для систем 𝗟𝗶𝗻𝘂𝘅. Тем не менее при настройке и запуске этой программы под 𝗪𝗶𝗻𝗱𝗼𝘄𝘀 с помощью 𝗰𝗺𝗱.𝗲𝘅𝗲, а не /𝗯𝗶𝗻/𝗯𝗮𝘀𝗵, подключающийся клиент не получает вывод команды из-за специфичной для 𝗪𝗶𝗻𝗱𝗼𝘄𝘀 обработки анонимных каналов. Далее описаны два решения этой проблемы.

Во-первых, можно настроить код для принудительного сброса 𝘀𝘁𝗱𝗼𝘂𝘁. Вместо непосредственного присваивания 𝗖𝗼𝗻𝗻 экземпляру 𝗰𝗺𝗱.𝗦𝘁𝗱𝗼𝘂𝘁 нужно реализовать собственный 𝗪𝗿𝗶𝘁𝗲𝗿, который обертывает 𝗯𝘂𝗳𝗶𝗼.𝗪𝗿𝗶𝘁𝗲𝗿 (буферизованный райтер) и явно вызывает его метод 𝗙𝗹𝘂𝘀𝗵 для принудительного сброса буфера. Пример использования 𝗯𝘂𝗳𝗶𝗼.𝗪𝗿𝗶𝘁𝗲𝗿 можно найти в разделе «Создание эхо-сервера» ранее в этой главе. Вот определение пользовательского райтера, 𝗙𝗹𝘂𝘀𝗵𝗲𝗿:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Тип 𝗙𝗹𝘂𝘀𝗵𝗲𝗿 реализует функцию 𝗪𝗿𝗶𝘁𝗲([]𝗯𝘆𝘁𝗲), которая записывает данные во внутренний буферизованный райтер, а затем сбрасывает вывод.

С помощью этой реализации пользовательского райтера можно настроить обработчик подключений на создание экземпляра и применение типа 𝗙𝗹𝘂𝘀𝗵𝗲𝗿 для 𝗰𝗺𝗱.𝗦𝘁𝗱𝗼𝘂𝘁:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Продолжение кода выше

Это решение хотя и вполне пригодно, но не очень элегантно. Несмотря на то что рабочий код для нас важнее, чем аккуратный, мы используем эту проблему как возможность рассказать о функции 𝗶𝗼.𝗣𝗶𝗽𝗲(). Она представляет собой синхронный канал в памяти 𝗚𝗼, который можно задействовать для подключения 𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗪𝗿𝗶𝘁𝗲𝗿:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Применение 𝗣𝗶𝗽𝗲𝗥𝗲𝗮𝗱𝗲𝗿 и 𝗣𝗶𝗽𝗲𝗪𝗿𝗶𝘁𝗲𝗿 позволяет избежать необходимости явного сброса райтера и синхронного подключения 𝘀𝘁𝗱𝗼𝘂𝘁 и 𝗧𝗖𝗣-соединения. Опять же понадобится переписать функцию обработчика:

Создание TCP-прокси Информационная безопасность, Хакеры, Программирование, Golang, Программист, Интернет, Длиннопост

Вызов 𝗶𝗼.𝗣𝗶𝗽𝗲 создает ридер и райтер, подключаемые синхронно, — любые данные, записываемые в райтер (в данном примере 𝘄𝗽), будут считаны ридером (𝗿𝗽). Поэтому сначала происходит присваивание райтера экземпляру 𝗰𝗺𝗱.𝗦𝘁𝗱𝗼𝘂𝘁, после чего используется 𝗶𝗼.𝗖𝗼𝗽𝘆(𝗰𝗼𝗻𝗻, 𝗿𝗽) для присоединения 𝗣𝗶𝗽𝗲𝗥𝗲𝗮𝗱𝗲𝗿 к 𝗧𝗖𝗣-соединению. Это делается с помощью горутины, предотвращающей блокирование кода. Любой стандартный вывод команды отправляется райтеру, после чего передается ридеру и далее через 𝗧𝗖𝗣-соединение. Как вам такая элегантность?

Таким образом, мы успешно реализовали «зияющую дыру безопасности» 𝗡𝗲𝘁𝗰𝗮𝘁 с позиции 𝗧𝗖𝗣-слушателя, ожидающего подключения. По тому же принципу можно реализовать эту функцию с позиции подключающегося клиента, перенаправляющего 𝘀𝘁𝗱𝗼𝘂𝘁 и 𝘀𝘁𝗱𝗶𝗻 локального исполняемого файла удаленному слушателю. Детали этого процесса мы оставим вам для самостоятельной реализации, но в общем они будут включать следующее:

  • установку подключения к удаленному слушателю через net.Dial(network, address string);

  • инициализацию Cmd через exec.Command(name string, arg ...string);

  • перенаправление свойств Stdin и Stdout для использования объекта net.Conn;

  • выполнение команды.

На этом этапе слушатель должен получить подключение. Любые передаваемые клиенту данные должны интерпретироваться на клиенте как 𝘀𝘁𝗱𝗶𝗻, а данные, получаемые слушателем, — как 𝘀𝘁𝗱𝗼𝘂𝘁.

На этом всё, ждите в ближайшее время больше статей.

ССЫЛКА НА ТЕЛЕГРАМ КАНАЛ АВТОРА

Показать полностью 21
[моё] Информационная безопасность Хакеры Программирование Golang Программист Интернет Длиннопост
0
6
PENTEST.DNA
PENTEST.DNA
1 год назад
Web-технологии

TCP, сканеры и прокси⁠⁠

TCP, сканеры и прокси Программирование, Информационная безопасность, Хакеры, Golang, IT, Программист, Длиннопост

В роли атакующего вы должны понимать принцип работы 𝗧𝗖𝗣, чтобы справляться с разработкой пригодных вариантов его конструкций, позволяющих определять открытые/закрытые порты, распознавать такие потенциально ошибочные результаты, как ложные срабатывания — например, 𝗦𝗬𝗡-флуд защиты, — и обходить ограничения на исходящий трафик посредством переадресации портов. В этой главе вы изучите основы 𝗧𝗖𝗣-коммуникаций в 𝗚𝗼, реализуете многопоточный правильно отрегулированный сканер портов, создадите 𝗧𝗖𝗣-прокси, который можно использовать для переадресации портов, а также воссоздадите 𝗡𝗲𝘁𝗰𝗮𝘁-функцию «зияющая дыра в безопасности».

В интернете куча информации которые раскрывают каждый нюанс 𝗧𝗖𝗣, включая такие темы, как потоки и структура пакетов, надежность, повторная сборка сегментов и многие другие. Настолько подробная детализация выходит за рамки этой темы, поэтому я рекомендую глубже изучить эту тему, после прочтения статьи.

TCP Handshaking:



В качестве напоминания мы начнем с основ. снизу будет показано, как 𝗧𝗖𝗣 при запросе порта использует процесс рукопожатия (𝗵𝗮𝗻𝗱𝘀𝗵𝗮𝗸𝗶𝗻𝗴), определяя, открыт порт, закрыт или фильтруется.

TCP, сканеры и прокси Программирование, Информационная безопасность, Хакеры, Golang, IT, Программист, Длиннопост

Основы рукопожатия в TCP


Если порт открыт, рукопожатие осуществляется в три этапа. Сначала клиент отправляет пакет 𝘀𝘆𝗻, определяющий начало сеанса связи. В ответ на это сервер отправляет 𝘀𝘆𝗻-𝗮𝗰𝗸, иначе говоря, подтверждение получения пакета 𝘀𝘆𝗻, предлагая клиенту завершить сеанс установки связи отправкой сигнала 𝗮𝗰𝗸, то есть встречного подтверждения получения ответа сервера. После этого может начаться обмен данными. Если же порт будет закрыт, сервер ответит пакетом 𝗿𝘀𝘁, а не 𝘀𝘆𝗻-𝗮𝗰𝗸. В случае, когда трафик фильтруется межсетевым экраном (брандмауэром), клиент обычно не получает от сервера ответа.

При написании сетевых протоколов важно понимать принцип работы этих пакетов. Соответствие выходных данных создаваемых вами инструментов этим низкоуровневым потокам пакетов поможет убедиться в правильной установке сетевого соединения и устранить потенциальные проблемы. Как вы увидите чуть позже, в коде можно легко допустить ошибки, не реализовав полный цикл рукопожатия при соединении «клиент — сервер», что приведет к неточным или вводящим в заблуждение результатам.



Обход брандмауэра с помощью переадресации портов:



Иногда в системах с целью ограничения доступа клиента к конкретным серверам и портам устанавливаются брандмауэры. В некоторых случаях эти ограничения можно обойти, используя промежуточную систему для проксирования в обход или через такой брандмауэр. Эта техника называется переадресацией портов.

Многие корпоративные сети ограничивают возможность подключения своих внутренних ресурсов к вредоносным сайтам. В качестве примера представьте такой сайт под названием 𝗲𝘃𝗶𝗹.𝗰𝗼𝗺. Если сотрудник компании попытается подключиться к нему напрямую, брандмауэр заблокирует его запрос. Но если у сотрудника есть собственная внешняя система, доступная через брандмауэр (например, 𝘀𝘁𝗮𝗰𝗸𝘁𝗶𝘁𝗮𝗻.𝗰𝗼𝗺), то он может задействовать ее для установки связи с 𝗲𝘃𝗶𝗹.𝗰𝗼𝗺. Этот принцип отражен снизу.

TCP, сканеры и прокси Программирование, Информационная безопасность, Хакеры, Golang, IT, Программист, Длиннопост

TCP-проски

Клиент подключается к удаленному хосту 𝘀𝘁𝗮𝗰𝗸𝘁𝗶𝘁𝗮𝗻.𝗰𝗼𝗺 через брандмауэр. Этот хост настроен на перенаправление соединений к хосту 𝗲𝘃𝗶𝗹.𝗰𝗼𝗺. Несмотря на то что брандмауэр запрещает прямые подключения к 𝗲𝘃𝗶𝗹.𝗰𝗼𝗺, описанная конфигурация позволяет клиенту обойти этот механизм защиты.

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



Написание TCP-сканера:



Один из эффективных способов концептуализировать понимание взаимодействия 𝗧𝗖𝗣-портов — это реализация их сканера. В процессе его создания вы увидите все шаги обмена рукопожатиями в 𝗧𝗖𝗣, а также эффекты от возникающих изменений состояний, которые позволяют определить, является ли порт доступным, закрытым или отфильтровывается.

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


Тестирование портов на доступность:


Первый шаг в создании сканера портов — понять процесс инициирования соединения от клиента к серверу. В рассматриваемом примере вы будете подключаться к 𝘀𝗰𝗮𝗻𝗺𝗲.𝗻𝗺𝗮𝗽.𝗼𝗿𝗴 — сервису проекта 𝗡𝗺𝗮𝗽𝟭 и сканировать его. Для этого мы с вами задействуем пакет 𝗚𝗼 𝗻𝗲𝘁: 𝗻𝗲𝘁.𝗗𝗶𝗮𝗹(𝗻𝗲𝘁𝘄𝗼𝗿𝗸, 𝗮𝗱𝗱𝗿𝗲𝘀𝘀 𝘀𝘁𝗿𝗶𝗻𝗴).

Первый аргумент — это строка, определяющая тип инициируемого соединения. Дело в том, что 𝗗𝗶𝗮𝗹 используется не только для 𝗧𝗖𝗣, но и для создания соединений, задействующих сокеты 𝗨𝗻𝗶𝘅, 𝗨𝗗𝗣 и протоколы 𝟰-го уровня, которые мы оставим в стороне, так как на основе всего нашего опыта будет достаточно просто сказать, что 𝗧𝗖𝗣 очень хорош. В этот аргумент можно передать несколько вариантов строк, но для краткости будем использовать строку 𝘁𝗰𝗽.

Второй аргумент указывает 𝗗𝗶𝗮𝗹(𝗻𝗲𝘁𝘄𝗼𝗿𝗸, 𝗮𝗱𝗱𝗿𝗲𝘀𝘀 𝘀𝘁𝗿𝗶𝗻𝗴) на хост, к которому вы хотите подключиться. Обратите внимание на то, что это одна строка, а не 𝘀𝘁𝗿𝗶𝗻𝗴 и 𝗶𝗻𝘁. Для соединений 𝗜𝗣𝘃𝟰/𝗧𝗖𝗣 она будет принимать форму 𝗵𝗼𝘀𝘁:𝗽𝗼𝗿𝘁. Например, если вам нужно подключиться к 𝘀𝗰𝗮𝗻𝗺𝗲.𝗻𝗺𝗮𝗽.𝗼𝗿𝗴 через 𝗧𝗖𝗣-порт 𝟴𝟬, то нужно указать 𝘀𝗰𝗮𝗻𝗺𝗲.𝗻𝗺𝗮𝗽.𝗼𝗿𝗴:𝟴𝟬.

Теперь вы знаете, как создать соединение, но как понять, что оно было успешным? Для этого выполняется проверка на ошибки: 𝗗𝗶𝗮𝗹(𝗻𝗲𝘁𝘄𝗼𝗿𝗸, 𝗮𝗱𝗱𝗿𝗲𝘀𝘀 𝘀𝘁𝗿𝗶𝗻𝗴) возвращает Conn и error. При этом error будет nil, если соединение установлено успешно. Так что для проверки вам просто нужно убедиться, что error равна nil. Вот теперь у вас есть все необходимые элементы для построения сканера портов, хотя и не особо корректного. В Основах рукопожатия в TCP показано, как все это объединить.

TCP, сканеры и прокси Программирование, Информационная безопасность, Хакеры, Golang, IT, Программист, Длиннопост

Простой сканер портов, сканирующий только один порт /dial/main.go

Выполнив этот код, вы должны увидеть сообщение 𝗖𝗼𝗻𝗻𝗲𝗰𝘁𝗶𝗼𝗻 𝘀𝘂𝗰𝗰𝗲𝘀𝘀𝗳𝘂𝗹 при условии наличия у вас доступа к великой информационной супермагистрали


Выполнение однопоточного сканирования:



Сканирование по одному порту за раз не особо полезно и малоэффективно, так как диапазон 𝗧𝗖𝗣-портов — от 𝟭 до 𝟲𝟱 𝟱𝟯𝟱. В целях же тестирования давайте пока просканируем порты от 𝟭 до 𝟭𝟬𝟮𝟰. Для этого можно использовать цикл 𝗳𝗼𝗿:

TCP, сканеры и прокси Программирование, Информационная безопасность, Хакеры, Golang, IT, Программист, Длиннопост

Теперь у вас есть 𝗶𝗻𝘁, но нужно помнить, что в качестве второго аргумента для 𝗗𝗶𝗮𝗹(𝗻𝗲𝘁𝘄𝗼𝗿𝗸, 𝗮𝗱𝗱𝗿𝗲𝘀𝘀 𝘀𝘁𝗿𝗶𝗻𝗴) требуется строка. Есть по меньшей мере два способа преобразования целого числа в строку. Первый — использовать пакет преобразования строк 𝘀𝘁𝗿𝗰𝗼𝗻𝘃. Второй — применить функцию 𝗦𝗽𝗿𝗶𝗻𝘁𝗳(𝗳𝗼𝗿𝗺𝗮𝘁 𝘀𝘁𝗿𝗶𝗻𝗴, 𝗮 ...𝗶𝗻𝘁𝗲𝗿𝗳𝗮𝗰𝗲{}) из пакета 𝗳𝗺𝘁, которая (аналогично своему собрату в 𝗖) возвращает 𝘀𝘁𝗿𝗶𝗻𝗴, сгенерированную из строки формата (𝗳𝗼𝗿𝗺𝗮𝘁 𝘀𝘁𝗿𝗶𝗻𝗴).


Создайте файл с кодом из листинга 𝟮.𝟮 и убедитесь в работоспособности цикла и функции генерации строки. Выполнение этого кода должно вывести 𝟭𝟬𝟮𝟰 строки, но утруждать себя их подсчетом не обязательно.

TCP, сканеры и прокси Программирование, Информационная безопасность, Хакеры, Golang, IT, Программист, Длиннопост

Сканирование 1024 портов scanme.nmap.org /tcp-scanner-slow/main.go

Теперь осталось только подставить переменную адреса из предыдущего кода в 𝗗𝗶𝗮𝗹(𝗻𝗲𝘁𝘄𝗼𝗿𝗸, 𝗮𝗱𝗱𝗿𝗲𝘀𝘀 𝘀𝘁𝗿𝗶𝗻𝗴) и протестировать доступность портов, реализовав такую же проверку ошибок, как в предыдущем разделе. Помимо этого, чтобы не оставлять успешные соединения открытыми, следует добавить логику их закрытия. Завершение соединений — это жест вежливости. Для этого вам нужно выполнить в 𝗖𝗼𝗻𝗻 вызов 𝗖𝗹𝗼𝘀𝗲(). Снизу показана полноценная реализация сканера портов.

TCP, сканеры и прокси Программирование, Информационная безопасность, Хакеры, Golang, IT, Программист, Длиннопост

Завершенный сканер портов /tcp-scanner-slow/main.go

Скомпилируйте и выполните этот код для выполнения легкого сканирования цели. Вы должны обнаружить пару открытых портов.



Параллельное сканирование:



Предыдущий сканер сканировал серию портов в один заход. Но вашей целью является проверка множества портов параллельно, что существенно ускорит его работу. Для этого мы воспользуемся горутинами. 𝗚𝗼 позволяет создавать столько горутин, сколько способна обработать ваша система, ограничиваясь только объемом доступной памяти.



Слишком быстрая версия сканера:



Самым прямолинейным способом создания параллельного сканера будет обернуть вызов 𝗗𝗶𝗮𝗹(𝗻𝗲𝘁𝘄𝗼𝗿𝗸, 𝗮𝗱𝗱𝗿𝗲𝘀𝘀 𝘀𝘁𝗿𝗶𝗻𝗴) в горутину. Чтобы собственными глазами увидеть последствия этого, создайте файл 𝘀𝗰𝗮𝗻-𝘁𝗼𝗼-𝗳𝗮𝘀𝘁.𝗴𝗼 с кодом который снизу.

TCP, сканеры и прокси Программирование, Информационная безопасность, Хакеры, Golang, IT, Программист, Длиннопост

Сканер, работающий слишком быстро /tcp-scanner-too-fast/main.go

При выполнении кода вы заметите, что программа завершается практически мгновенно:

TCP, сканеры и прокси Программирование, Информационная безопасность, Хакеры, Golang, IT, Программист, Длиннопост

Этот код запускает по одной горутине для каждого соединения, а основная горутина не знает, что нужно ждать окончания его установки. В связи с этим выполнение кода завершается, как только цикл 𝗳𝗼𝗿 заканчивает перебор, что происходит быстрее, чем сеть успевает осуществить обмен пакетами между кодом и всеми целевыми портами. Поэтому вы не получите точных результатов для портов, чьи пакеты находились в процессе обмена.


Исправить это можно несколькими способами. Первый — использовать 𝗪𝗮𝗶𝘁𝗚𝗿𝗼𝘂𝗽 из пакета 𝘀𝘆𝗻𝗰, предоставляющий потокобезопасный способ управления параллельным выполнением. 𝗪𝗮𝗶𝘁𝗚𝗿𝗼𝘂𝗽 — это тип структуры, который создается так:

TCP, сканеры и прокси Программирование, Информационная безопасность, Хакеры, Golang, IT, Программист, Длиннопост

Создав 𝗪𝗮𝗶𝘁𝗚𝗿𝗼𝘂𝗽, вы можете вызвать для этой структуры несколько методов. Первый метод — 𝗔𝗱𝗱(𝗶𝗻𝘁), увеличивающий внутренний счетчик согласно переданному числу. Следующий — метод 𝗗𝗼𝗻𝗲(), уменьшающий счетчик на 𝟭. И наконец, метод 𝗪𝗮𝗶𝘁(), блокирующий выполнение горутины, в которой вызывается, запрещая дальнейЭтот вариант кода по большому счету остался неизменным. Тем не менее здесь мы добавили код, явно отслеживающий оставшуюся работу. В этой версии программышее выполнение, пока внутренний счетчик не достигнет нуля. Эти вызовы можно совмещать, гарантируя, что основная горутина дождется завершения всех соединений.


Синхронизированное сканирование с помощью WaitGroup:

TCP, сканеры и прокси Программирование, Информационная безопасность, Хакеры, Golang, IT, Программист, Длиннопост

Синхронизированный сканер, использующий WaitGroup /tcp-scanner-wg-too-fast/main.go

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

создаем 𝘀𝘆𝗻𝗰.𝗪𝗮𝗶𝘁𝗚𝗿𝗼𝘂𝗽, выступающую в качестве синхронизированного счетчика. Мы увеличиваем этот счетчик через 𝘄𝗴.𝗔𝗱𝗱(𝟭) при каждом создании горутины для сканирования порта. При этом отложенный вызов 𝘄𝗴.𝗗𝗼𝗻𝗲() уменьшает этот счетчик при завершении каждой единицы работы. Функция 𝗺𝗮𝗶𝗻() вызывает 𝘄𝗴.𝗪𝗮𝗶𝘁(), который блокирует выполнение, пока не будет выполнена вся работа и счетчик не достигнет нуля.

Эта версия программы уже лучше, но по-прежнему имеет недостатки. Если запустить ее несколько раз для разных хостов, можно получить несогласованные результаты. Одновременное сканирование чрезмерного количества хостов или портов может привести к тому, что ограничения системы или сети исказят результаты. Попробуйте изменить в коде значение 𝟭𝟬𝟮𝟰 на 𝟲𝟱𝟱𝟯𝟱 и укажите адрес целевого сервера как 𝟭𝟮𝟳.𝟬.𝟬.𝟭. При желании можете использовать 𝗪𝗶𝗿𝗲𝘀𝗵𝗮𝗿𝗸 или 𝘁𝗰𝗽𝗱𝘂𝗺𝗽, чтобы увидеть, насколько быстро открываются эти соединения.



Сканирование портов с помощью пула воркеров:



Чтобы избежать несогласованности, можно задействовать для управления параллельным выполнением пул горутин. С помощью цикла 𝗳𝗼𝗿 вы создаете определенное количество воркеров горутин в качестве пула. Затем в потоке 𝗺𝗮𝗶𝗻() с помощью канала обеспечиваете работу.

Для начала создайте новую программу, которая использует канал 𝗶𝗻𝘁, содержит 𝟭𝟬𝟬 воркеров и выводит их результаты на экран. При этом задействуйте 𝗪𝗮𝗶𝘁𝗚𝗿𝗼𝘂𝗽 для блокирования выполнения.

Создайте начальную заглушку кода для функции 𝗺𝗮𝗶𝗻, а над ней напишите функцию,приведенную в коде ниже

TCP, сканеры и прокси Программирование, Информационная безопасность, Хакеры, Golang, IT, Программист, Длиннопост

Функция с воркером для выполнения задачи.

Функция 𝘄𝗼𝗿𝗸𝗲𝗿(𝗶𝗻𝘁, *𝘀𝘆𝗻𝗰.𝗪𝗮𝗶𝘁𝗚𝗿𝗼𝘂𝗽) получает два аргумента: канал типа 𝗶𝗻𝘁 и указатель на 𝗪𝗮𝗶𝘁𝗚𝗿𝗼𝘂𝗽. Канал будет использоваться для получения работы, а 𝗪𝗮𝗶𝘁𝗚𝗿𝗼𝘂𝗽 — для отслеживания завершения одной ее единицы.

Далее добавьте функцию 𝗺𝗮𝗶𝗻(), приведенную в коде ниже, которая будет управлять рабочей нагрузкой и обеспечивать работу функции 𝘄𝗼𝗿𝗸𝗲𝗿(𝗶𝗻𝘁, *𝘀𝘆𝗻𝗰.𝗪𝗮𝗶𝘁𝗚𝗿𝗼𝘂𝗽).

TCP, сканеры и прокси Программирование, Информационная безопасность, Хакеры, Golang, IT, Программист, Длиннопост

Базовый пул воркеров /tcp-sync-scanner/main.go

Сначала создается канал с помощью 𝗺𝗮𝗸𝗲(). В 𝗺𝗮𝗸𝗲() в качестве второго параметра передается значение 𝟭𝟬𝟬. Это добавляет каналу буферизацию, то есть в него можно будет отправлять элемент и не ждать, пока получатель этот элемент прочтет. Буферизованные каналы идеально подходят для поддержания и отслеживания работы нескольких производителей и потребителей. Емкость канала определяется как 𝟭𝟬𝟬. Значит, он может вместить 𝟭𝟬𝟬 элементов, до того как отправитель будет заблокирован. Это дает небольшой прирост производительности, поскольку все воркеры смогут запускаться сразу.

Далее с помощью цикла 𝗳𝗼𝗿 запускается заданное число воркеров — в данном случае 𝟭𝟬𝟬. В функции 𝘄𝗼𝗿𝗸𝗲𝗿(𝗶𝗻𝘁, *𝘀𝘆𝗻𝗰.𝗪𝗮𝗶𝘁𝗚𝗿𝗼𝘂𝗽) с помощью 𝗿𝗮𝗻𝗴𝗲 происходит непрерывное циклическое получение данных из канала 𝗽𝗼𝗿𝘁𝘀, завершающееся только при закрытии канала. Обратите внимание: пока воркер никакой работы не выполняет — это произойдет чуть позже. Последовательно перебирая порты в функции 𝗺𝗮𝗶𝗻(), вы отправляете порт через канал 𝗽𝗼𝗿𝘁𝘀 воркеру. По завершении всей работы закрываете канал.

Запустив эту программу, вы увидите, как на экран выводятся числа. Здесь можно заметить кое-что интересное, а именно то, что выводятся они в конкретном порядке. Добро пожаловать в прекрасный мир многопоточности!



Многоканальная связь:



Чтобы завершить создание сканера, можно вставить код, использованный ранее в этом разделе, и это вполне сработает. Но в таком случае выводимые порты будут не отсортированы, так как сканер станет проверять их не по порядку. Решить эту проблему можно, реализовав упорядоченную передачу результатов сканирования в основной поток через дополнительный. Это изменение к тому же позволит полностью устранить зависимость от 𝗪𝗮𝗶𝘁𝗚𝗿𝗼𝘂𝗽, так как теперь у вас будет другой метод для отслеживания завершения. Например, если вы сканируете 𝟭𝟬𝟮𝟰 порта, то делаете по каналу воркера 𝟭𝟬𝟮𝟰 передачи, после чего снова выполняете 𝟭𝟬𝟮𝟰 передачи с результатами работы обратно в основной поток. Поскольку количество отправленных единиц работы и полученных результатов совпадает, программа понимает, когда нужно закрывать каналы и, следовательно, отключать воркеры.


Эта модификация кода представлена в коде ниже, которым завершается создание сканера.

TCP, сканеры и прокси Программирование, Информационная безопасность, Хакеры, Golang, IT, Программист, Длиннопост

Сканирование портов через несколько каналов /tcp-scanner-final/main.go

Функция 𝘄𝗼𝗿𝗸𝗲𝗿(𝗽𝗼𝗿𝘁𝘀, 𝗿𝗲𝘀𝘂𝗹𝘁𝘀 𝗰𝗵𝗮𝗻 𝗶𝗻𝘁) была изменена для получения двух каналов. Остальная логика почти полностью осталась прежней, за исключением того, что в случае закрытого порта вы отправляете ноль, а в случае открытого — значение этого порта. Кроме того, здесь вы создаете отдельный канал для передачи результатов от воркера в основной поток. Затем результаты сохраняются в срез, что позволяет выполнить их сортировку. Далее вам нужно реализовать отправку данных воркера в отдельной горутине, потому что цикл сбора результатов должен начаться до того, как сможет продолжиться выполнение более 𝟭𝟬𝟬 единиц работы.

Этот цикл получает по каналу 𝗿𝗲𝘀𝘂𝗹𝘁𝘀 𝟭𝟬𝟮𝟰 передачи. Если порт не равен 𝟬, он добавляется в срез. После закрытия каналов вы используете сортировку для упорядочивания среза открытых портов. Далее остается лишь перебрать срез и вывести открытые порты на экран.

Вот мы и написали высокопроизводительный сканер портов. Уделите время экспериментированию с кодом — в частности, с количеством воркеров. Чем их больше, тем быстрее должна выполняться программа. Но если их окажется слишком много, результаты станут ненадежными. При написании инструментов, которые будут применять другие люди, вам нужно использовать грамотное предустановленное значение, которое ориентировано на надежность, а не на скорость. При этом также следует предоставлять пользователям опцию самостоятельного выбора количества воркеров.

В полученную программу можно внести пару улучшений. Во-первых, вы отправляете по каналу 𝗿𝗲𝘀𝘂𝗹𝘁𝘀 результат сканирования каждого порта, что необязательно. Альтернативное решение потребует написания более сложного кода, который будет использовать дополнительный канал не только для отслеживания воркеро. Вам может потребоваться, чтобы сканер умел парсить строки с портами, например 𝟴𝟬,𝟰𝟰𝟯,𝟴𝟬𝟴𝟬,𝟮𝟭-𝟮𝟱, наподобие тех, что могут быть переданы в 𝗡𝗺𝗮𝗽. Я предлагаю вам освоить этот прием самостоятельно.

P.S: в следующей статье мы рассмотрим создание TCP-прокси.

ССЫЛКА НА ТЕЛЕГРАМ КАНАЛ АВТОРА

Показать полностью 13
[моё] Программирование Информационная безопасность Хакеры Golang IT Программист Длиннопост
1
Посты не найдены
О Нас
О Пикабу
Контакты
Реклама
Сообщить об ошибке
Сообщить о нарушении законодательства
Отзывы и предложения
Новости Пикабу
RSS
Информация
Помощь
Кодекс Пикабу
Награды
Команда Пикабу
Бан-лист
Конфиденциальность
Правила соцсети
О рекомендациях
Наши проекты
Блоги
Работа
Промокоды
Игры
Скидки
Курсы
Зал славы
Mobile
Мобильное приложение
Партнёры
Промокоды Biggeek
Промокоды Маркет Деливери
Промокоды Яндекс Путешествия
Промокоды М.Видео
Промокоды в Ленте Онлайн
Промокоды Тефаль
Промокоды Сбермаркет
Промокоды Спортмастер
Постила
Футбол сегодня
На информационном ресурсе Pikabu.ru применяются рекомендательные технологии