При прохождения очередного собеседования мне задали небольшое тестовое задание, написать на Go сетевой сканер открытых портов. Задание в принципе не сложное, но одним из условий было, что в качестве параметра может быть передан как IP-адрес, так и диапазон сетевых адресов в виде сетевой маски: 192.168.8.0/21.

Тема скорее всего очень тривиальная для сетевых инженеров и администраторов и, наверно, даже скучная. Моя цель просто изложить тут алгоритм перевод диапазона IP в сетевую маску (дальше по тексту CIDR) и обратно из CIDR в диапазон адресов.

Немного теории, но тем, кто уже знаком с терминами можно пропустить и сразу перейти к алгоритму.

И так, что такое IP я думаю всем понятно и пояснять не нужно. Теперь что такое CIDR (или сетевая маска). Представим, что у нас есть IP: «192.168.11.10». На самом деле это 8-битовые значения разделенные точками, и каждая отдельная часть это так называемый октет. Понятно, что IP можно представить в виде 32-битового числа.

image

Именно такое число и передается в IP-пакете. Теперь давайте представим, что у нас есть подсеть состоящая из 8 хостов — 192.168.11.0 до 192.168.11.7 (Note: 192.168.11.0 нельзя использовать в качестве адреса какого-либо сетевого интерфейса, так как этот адрес используется как идентификатор подсети, поэтому фактически адресов будет 7, но для нашего пример это пока не имеет значения.)
Понятно, что подсетей может быть несколько в рамках одном большой сети и отправлять пакет внутри своей подсети нужно напрямую, а скажем если пакет нужно направить в другую подсеть допусти на IP: 192.168.11.22, то его нужно направить сетевому маршрутизатору, который перенаправить этот пакет в другую подсеть. Хранить на хосте отправителе все адреса своей подсети накладно и бессмысленно, поэтому мы просто храним сетевую маску и для выше обозначенной сети из 8 хостов она будет равна — 255.255.255.248. Теперь если разложить сетевую маску на биты — получим 29 единиц и 3 нулей.

image

Маска подсети никогда не может перемешивать “1” и “0”, поэтому всегда сначала идет последовательность “1”, а потом последовательность “0”. Теперь диапазон выше обозначенных адресов 192.168.11.0 до 192.168.11.7, можно представить как <IP адрес подсети>/<Маска подсети>, т.e. 192.168.11.0/29. Это и есть бесклассовая адресация или Classless Inter-Domain Routing (CIDR) и в этой компактной форме можно представить любой диапазон IP адресов. Относительно правильного разбиения на подсети и маршрутизации имеет смысл обратиться к специализированной литературе и не является целью данной статьи — я надеюсь меня простят сетевые администраторы.

И так сам алгоритм c комментариями приведен ниже. Сразу скажу, что для диапазона 216.58.192.12 — 216.58.192.206, нет возможности разбить на одну подсеть и алгоритм сразу разобьет диапазон на несколько подсетей:
{
«216.58.192.12/30»,
«216.58.192.16/28»,
«216.58.192.32/27»,
«216.58.192.64/26»,
«216.58.192.128/26»,
«216.58.192.192/29»,
«216.58.192.200/30»,
«216.58.192.204/31»,
«216.58.192.206/32”
}

Convert IPv4 range into CIDR
// Convert IPv4 range into CIDR
func iPv4RangeToCIDR(ipStart string, ipEnd string) (CIDRs []string, err error) {

  cidr2mask := []uint32{
     0x00000000, 0x80000000, 0xC0000000,
     0xE0000000, 0xF0000000, 0xF8000000,
     0xFC000000, 0xFE000000, 0xFF000000,
     0xFF800000, 0xFFC00000, 0xFFE00000,
     0xFFF00000, 0xFFF80000, 0xFFFC0000,
     0xFFFE0000, 0xFFFF0000, 0xFFFF8000,
     0xFFFFC000, 0xFFFFE000, 0xFFFFF000,
     0xFFFFF800, 0xFFFFFC00, 0xFFFFFE00,
     0xFFFFFF00, 0xFFFFFF80, 0xFFFFFFC0,
     0xFFFFFFE0, 0xFFFFFFF0, 0xFFFFFFF8,
     0xFFFFFFFC, 0xFFFFFFFE, 0xFFFFFFFF,
  }
 
// Переведем IP в беззнаковые целые числа. 
  ipStartUint32 := iPv4ToUint32(ipStart)
  ipEndUint32 := iPv4ToUint32(ipEnd)

// Если диапазон задан неверно, просто вернем ошибку.
  if ipStartUint32 > ipEndUint32 {
     log.Fatalf("start IP:%s must be less than end IP:%s", ipStart, ipEnd)
  }

  for ipEndUint32 >= ipStartUint32 {
     maxSize := 32

// Определим максимальную маску подсети доступную для текущего IP адреса. 
     for maxSize > 0 {
        maskedBase := ipStartUint32 & cidr2mask[maxSize - 1]

        if maskedBase != ipStartUint32 {
           break
        }
        maxSize--
     }

// Проверим, если маска превышает диапазон указанный в конечном IP адресе. И если превышает, проведем коррекцию. 
     x := math.Log(float64(ipEndUint32 - ipStartUint32 + 1)) / math.Log(2)
     maxDiff := 32 - int(math.Floor(x))
     if maxSize < maxDiff {
        maxSize = maxDiff
     }

// Сохраним CIDR 
  CIDRs = append(CIDRs,  uInt32ToIPv4(ipStartUint32) + "/" +  strconv.Itoa(maxSize))

// Увеличим диапазон на размерность подсети и повторим цикл. 
     ipStartUint32 += uint32(math.Exp2(float64(32 - maxSize)))
  }
  return CIDRs, err
}



Теперь что касается обратного алгоритма, когда нужно перевести такой же диапазон бесклассовой адресации обратно в IP. Лично в моем тестовом задании этот алгоритм не понадобился и возможно кто-то из сетевых администраторов прокомментирует, насколько он нужен в реальной жизни, но я решил написать сразу и тот и другой. Алгоритм до элементарного прост, вы должны в начальному IP подсети прибавить ее размерность.

Convert CIDR to IPv4 range
// Convert CIDR to IPv4 range
func CIDRRangeToIPv4Range(CIDRs []string) (ipStart string, ipEnd string, err error) {
  var ip uint32        // ip address
  var ipS uint32     // Start IP address range
  var ipE uint32         // End IP address range

  for _, CIDR := range CIDRs {
     cidrParts := strings.Split(CIDR, "/")

     ip = iPv4ToUint32(cidrParts[0])
     bits, _ := strconv.ParseUint(cidrParts[1], 10, 32)

     if ipS == 0 || ipS > ip {
        ipS = ip
     }

     ip = ip | (0xFFFFFFFF >> bits)

     if ipE < ip {
        ipE = ip
     }
  }

  ipStart = uInt32ToIPv4(ipS)
  ipEnd = uInt32ToIPv4(ipE)

  return ipStart, ipEnd, err
}



Note: только для Go разработчиков: алгоритм можно сделать еще более производительным, если возвращать данные в формате — (IP, *IPNet, error), но для универсальности я возвращаю данные как string.


Код сетевого сканера лежит тут: GitHub. Если поставите звездочку, буду благодарен, но только, если я ее заслужил :))

Ссылки:

Комментарии (2)


  1. xytop
    20.03.2018 10:51

    Стоило указать, что в Go есть стандартные функции для парсинга CIDR (net.ParseCIDR) и работы с диапазонами адресов (net.IPNet). И на собеседовании, вероятно, подразумевалось именно из использование.


    1. BOOTLOADER Автор
      20.03.2018 18:28

      Да, я использую net.ParseCIDR, но если мне в качестве параметров передается скажем 216.58.192.12 — 216.58.192.34, то мне его нужно сначала перевести в набор подсетей {216.58.192.12/30, 216.58.192.16/28, 216.58.192.32/31, 216.58.192.34/32}, потому уже работать с ними с помощью метода net.ParseCIDR.