И так передо мной в одном из проектов была поставлена задача, необходимо анализировать трафик, который отправлен из RouterOS с помощью sniff утилиты или из правила mangle sniff-tzsp

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

/ip firewall mangle
add action=sniff-tzsp chain=prerouting in-interface=Bridge-Local sniff-target=172.20.17.254 sniff-target-port=37008

Но данный софт, требовал только нахождения портов если протокол поддерживает порты, т.е. если это udp или tcp, необходимо было также посчитать объём трафика и что-то сделать с данной информацией.

Я же покажу как разобрать набор байтов и получить из них необходимую информацию.

Разбирать мы будем на примере языка GO без различных тонкостей оптимизации и прочего. Задача не в оптимизации, а в разборке. Также мы не будем пользоваться различными готовыми пакетами, чтобы не захламлять код и не увеличивать объём кода, тем более что зачастую многие пакеты универсальны и заточены под «всё и сразу».

И так начнём с простейшей задачи подготовить код.

package main
import (
    "encoding/binary"
   "errors"
   "log"
   "net"
)

func main(){

}

Объявили пакет main, уточнили какие пакеты мы будем использовать, дело в том некоторый год, нам в любом случае необходимо использовать. Мы не будем сходить сума и писать всё с нуля.

Ну а также функция main та самая функция, которая будем вызвана в момент запуска программы.

Далее я буду описывать содержимое функции main.

Слушаем порт

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

udpAddr, err := net.ResolveUDPAddr("udp4",”172.20.17.254:37008”)

Мы объявили переменную udpAddr которая имеет подготовленный вид, в случае если введённые данные не смогут быть преобразованы в IP порт, то переменная err будет содержать текст ошибки почему так произошло. Естественно, если данный метод отправил ошибку, нам необходимо вывести ошибку и завершить программу с кодом 1, т.е. явно указывая операционной системе, что программа завершилась с ошибкой.

go udpAddr, err := net.ResolveUDPAddr("udp4",”172.20.17.254:37008”) if err != nil { log.Fatal(err) }

Сразу за объявленной переменной, мы проверяем если переменная err содержит какой-то текст, то объявляем фатальную ошибку и выводим текст ошибки. Log.Fatal за собой прерывает выполнение приложения с кодом 1.

Очень интересный момент, заключается в том, что порт храниться как число int, хотя для этих целей лучше бы естественно подошёл тип uint16, так как порт имеет длину в 16 бит и не может быть более 65535, а int в свою очередь это 32 бита.

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

conn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
    log.Fatal(err)
}

С этого момента наша программа начинает слушать указанный IP адрес и порт с помощью протокола UDP.

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

buf := make([]byte, 10000)

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

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

for {
  l, _, err := conn.ReadFrom(buf)
  if err != nil {
    continue
  }

  _,err = Parse(buf[:l])
  if err != nil {
    continue
  }
}

Разберём некоторые нюансы по поводу содержимого цикла.

Так как у цикла нет условия он будет выполняться вечно.

l, _, err := conn.ReadFrom(buf)

Переменная l содержит длину данных, которые содержаться в буфере

if err != nil {
    continue
}

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

_,err = Parse(buf[:l])
if err != nil {
    continue
}

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

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

В не main функции объявляем ещё одну функцию

func Parse(b []byte)(p myip, err error){

} 

Указываем, что на входе идут набор байтов и они будут содержаться в переменной b, а на выходе мы отдадим переменную ip с собственным типом данных и ошибку при её наличии.

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

Объявим новый тип данных myip, объявляем перед функцией main

type myip struct {
  srcaddr, dstaddr []byte
  protocol byte
  size, srcport, dstport uint16
}

Наша структура будет состоять из 6 полей, адреса мы будем хранить в байтах, номер протокола в одном байте, так как он умещается в 8 бит, порты и размер данных до 65535 так как они умещаются в два байта.

И так приступим к написанию нашей функции, которая будет работать напрямую с байт кодом. Будем писать внутри функции Parse.

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

TZSP, Tazmen Sniffer Protocol

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

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

Описание заголовков можете найти на вики https://en.wikipedia.org/wiki/TZSP.

Если мы внимательно посмотрим на описание.

golang-write-tzsp-sniff by vasilev kirill

Первый байт всегда должны быть равен одному, так версия всегда должна быть 1.

Следующий байт описывает назначение трафика и нас интересует только тип со значением ноль (0) – полученные данные.

Дальше два байта описывают номер протокола, который инкапсулирован в tzsp протокол, нас интересует только ethernet номер 1, а так как два байта то 0 1

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

И так наш первый код в нашей функции будем проверка, а совпадает ли tzsp заголовок с необходимым.

if bytes.Compare(b[:5],[]byte{1,0,0,1,1} ) != 0 { return myip{}, errors.New("It’s not incapsulation ethrnet in TZSP") }

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

В нашем коде есть один нюанс, у нашей пока простенькой программке есть один момент, если на наш порт придёт пакет длинной менее чем 5 байт, то наша программка упадёт в panic так как она не может сделать выборку первых пяти байт из набора байт в переменной b так как их попросту меньше чем необходимое количество.

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

if l < 5 {
    continue
}

Поместим проверку, перед вызовом функции Parse внутри функции main. После данной процедуры, мы гарантируем, что в нашу обработку не попадёт, пакет которые не соответствует нашему ожиданию.

Поехали дальше.

Ethernet

Так как мы знаем, что первые пять байт это протокол tzsp, то с уверенностью можем сказать, что b[5:] содержит ethernet заголовок и какие-то данные в нём. Но при этом размер заголовка ethernet фиксированный и равен 14 байт, кроме тех случаев, когда используется vlan или q-in-q, но мы сразу оговорили, что нас интересует только IP протокол без каких-либо инкапсуляций. Поэтому мы должны проверить, что срез b[5:19] представляет из себя ethernet и дальнейшие данные это IP протокол. Открываем wiki https://en.wikipedia.org/wiki/Ethernet_frame и находим структуру ethernet протокола, и видим, что он состоит из двух мак адресов и протокола, протокол занимает два последних байта, при этом протокол нас интересует только ipv4 а его номер 0x0800, вот его мы и будем проверять EtherType.

golang-write-tzsp-sniff by vasilev kirill

if binary.BigEndian.Uint16(b[17:19]) != 0x0800 {
    return myip{}, errors.New("incapsulation in ethernet protocol not IPv4" )
}

Преобразуем два последних байта из диапазона 17:19 в Uint16 число и сверяем с номером ipv4 протокола, если не соответствует, прерываем выполнения функции и возвращаем ошибку.

Обратите внимание, что размер данных с которыми мы работаем уже составляет 19 байт, поэтому необходимо исправить в функции main l < 5 на l < 19. Защита от банальной ошибки, на первых данных, и чтобы лишний раз не гонять данные.

IPv4

Мы знаем, что в срезе наших данных b[19:] находится IPv4 заголовок и какие-то данные. Приступим к получению данных заголовка.

Опять же снова открываем wiki https://en.wikipedia.org/wiki/IPv4#Header и изучаем заголовок IPv4 протокола.

Первым делом мы видим, то в первом байте есть сразу два значения версия протокола, которая всегда должна соответствовать значению 4, и длинна заголовка. В языке go нет типа значения меньше, чем uint8.

golang-write-tzsp-sniff by vasilev kirill

И так в байте под номером 19 содержится два значения, первые 4 бита отвечают за версию ipv4, вторые\младшие 4 бита отвечают за размер заголовка.

Так как один байт это 8 бит, то для получения старших 4 бит, нам необходимо сдвинуть на 4 бита в право значение в 19 байте, тем самым останется, только 4 бита, которые были слева, делается очень просто b[19] >> 4

И соответственно в протоколе записано, что ВСЕГДА должно быть значение равно 4, поэтому проверим сразу.

if b[19] >> 4 != 4 {
 return myip{}, errors.New("ip header not version 4" )
}

С размером немного посложнее, нам необходимо применить маску или просто побитовое сравнение И, возьмём байт 0F, его двоичное представление равно набору бит в таком порядке 00001111, соответственно если мы применим логическое И то первые\старшие четыре бита заменятся на нули, в младшие 4 бита останутся в исходном состоянии. Вспомните как рассчитывается маска, аналогичный принцип.

b[19] & 0x0F

В большинстве случаев, вы будете получать число 5, это число обозначает кол-во групп по 32 бита, в которые должен уместиться заголовок.

Обращаю ваше внимание, что заголовок IPv4 не обязательно должен быть 20 байт, это его минимум, но не максимум. Так как под значения размера заголовка отведено 4 бита, то максимальное число, которое может уместиться это 2^4 = 16, а 16 это 16 групп по 32 бита, 32 бита можно описать как 4 байта, соответственно максимально возможный размер заголовка ipv4 составляет 16*4 = 64 байта.

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

headersize := b[19] & 0x0F * 4 //header size in byte
if headersize < 20 {
    myip{}, errors.New("ip header less 20 byte" )
}

Размер IPv4 заголовка не может быть меньше, чем 20 байт и не более 64 байт. Поправим наше условие, что пакет не должен быть менее чем, 5+14+20 байт. = 39 байт.

В нашем случае получается, что весь заголовок IPv4 находится в срезе b[19:19+headersize]

Объявляем наконец-то нашу переменную с типом myip

var iph = myip{}

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

ipheader := b[19:19+headersize]

Снова возвращаемся к структуре заголовка IP пакета, видим, что третий и четвёртый байт, определяют размер пакета вместе с заголовком, два байта это 16 бит, следует что размер пакета не может быть более чем 65535 так как 2^16 = 65535, получим его и сразу запишем в нашу переменную.

iph.size = binary.BigEndian.Uint16(ipheader[2:4])

Так же для определения протокола, который передаётся, внутри ipv4 пакета, мы должны определить его, он храниться в одном байте, и он по счёту девятый.

golang-write-tzsp-sniff by vasilev kirill

iph.protocol = ipheader[9]

Сразу проверим, если номер протокола ноль, то прерываем работу функции и отправляем ошибку.

if iph.protocol == 0 {
    return myip{}, errors.New("protocol not supported" )
}

Дальше определяем IP адреса в заголовке. Так как адрес IP имеет длину 32 бита, что эквивалентно 4 байтам, а минимальный размер пакета 20, если что-то и есть в заголовке после 20 байта, то это дополнительный опции. Байты 12-16 будут соответствовать IP адресу источника, а следующие 4 байта адресу назначения.

iph.srcaddr = ipheader[12:16]
iph.dstaddr = ipheader[16:20] 

Далее нам необходимо переделить порты, если, конечно, протокол позволяет использовать порты. Надо напомнить, что протокол tcp соответствует номеру 6, udp номеру соответствует номеру 17. В других протоколах портов не предусмотрено.

Я приведу пример, а ниже опишу почему именно так.

if iph.protocol == 6 || iph.protocol == 17 {
    iph.srcport = binary.BigEndian.Uint16(b[19+headersize: 19+headersize + 2])
    iph.dstport = binary.BigEndian.Uint16(b[19+headersize+2: 19+headersize + 4])
}

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

19+headersize – определяет начало следующего заголовка.

Осталось только вернуть наш результат.

return iph, nil

А также немного оптимизации, так как мы знаем, что 5 байт, уходит на tzsp заголовок, 14 байт уйдёт на ethernet заголовок, от 20 до 64 байт уйдёт на ipv4 заголовок и ещё 4 байта мы будем использовать при необходимости для определения портов в протоколах tcp и udp, в таком случае нам нет необходимости делать буфер больше чем сумма всех данных которые мы используем.

Изменим размер буфера на 5+14+64+4 = 87байт.

Собственно всё, осталось только как-то использовать эти данные, давайте выведем в консоль информацию, без какого-либо форматирования, просто для наглядности.

Перед return log.Println(iph)

И ниже код целиком.

package main

import (
    "bytes"
    "encoding/binary"
    "errors"
    "log"
    "net"
)

type myip struct {
    srcaddr,dstaddr []byte
    protocol byte
    size, srcport, dstport uint16
}

func main(){

    udpAddr, err := net.ResolveUDPAddr("udp4","172.20.17.254:37008")
    if err != nil {
        log.Fatal(err)
    }
    conn, err := net.ListenUDP("udp", udpAddr)
    if err != nil {
        log.Fatal(err)
    }
    buf := make([]byte, 87)
    for {
        l, _, err := conn.ReadFrom(buf)
        if err != nil {
            continue
        }
        if l < 39 {
            continue
        }
        _,err = Parse(buf[:l])
        if err != nil {
            continue
        }
    }
}

func Parse(b []byte)(p myip, err error){
    if bytes.Compare(b[:5],[]byte{1,0,0,1,1} ) != 0 {
        return myip{}, errors.New("It's not incapsulation ethrnet in TZSP")
    }

    if binary.BigEndian.Uint16(b[17:19]) != 0x0800 {
        return myip{}, errors.New("incapsulation in ethernet protocol not IPv4" )
    }
    if b[19] >> 4 != 4 {
        return myip{}, errors.New("ip header not version 4" )
    }
    headersize := b[19] & 0x0F * 4 //header size in byte
    if headersize < 20 {
        //ip header less  20byte
        return myip{}, errors.New("ip header less  20byte")
    }
    var iph = myip{}
    ipheader := b[19:19+headersize]
    iph.size = binary.BigEndian.Uint16(ipheader[2:4])
    iph.protocol = ipheader[9]
    if iph.protocol == 0 {
        return myip{}, errors.New("protocol not supported" )
    }

    if iph.protocol == 0 {
        return myip{}, errors.New("protocol not supported" )
    }

    iph.srcaddr = ipheader[12:16]
    iph.dstaddr = ipheader[16:20]

    if iph.protocol == 6 ||  iph.protocol == 17 {
        iph.srcport = binary.BigEndian.Uint16( b[19+headersize : 19 + headersize + 2])
        iph.dstport = binary.BigEndian.Uint16( b[19+headersize + 2: 19 + headersize + 4])
    }
    log.Println(iph)
    return iph, nil
}

Рассказать друзьям

Чатик телеграм

@mikrotikme