Генерация гео конфига для nginx

Появилась у меня задача быстро сгенерировать конфигурационный файл для гео модуля nginx. Ниже вариант решения.

Немного о сетях.

Internet Protocol или IP (англ. internet protocol — межсетевой протокол) — маршрутизируемый сетевой протокол, протокол сетевого уровня семейства («стека») TCP/IP.  В современной сети Интернет используется IP четвёртой версии, также известный как IPv4. В протоколе IP этой версии каждому узлу сети ставится в соответствие IP-адрес длиной 4 октета (4 байта). При этом компьютеры в подсетях объединяются общими начальными битами адреса. Количество этих бит, общее для данной подсети, называется маской подсети (ранее использовалось деление пространства адресов по классам — A, B, C; класс сети определялся диапазоном значений старшего октета и определял число адресуемых узлов в данной сети, сейчас используется бесклассовая адресация). На вход мы получаем массив из двух элементов: сеть (например 192.168.1.0/31) и идентификатор. © Wikipedia

Требования на входе:

  • Данными могут быть только подсети в правильном cidr формате.
  • Отсортированы в порядке возрастания (при использовании базы данных это не сложно).

Требования на выходе: отсортированный список непересекающихся диапазонов в файле минимального размера. Формат: начальный адрес подсети – конечный адрес идентификатор (192.168.1.1-192.168.1.126 336).

nginx ищет соответствие определенного ip адреса и диапазонов из файла. Возвращает идентификатор.

Для начала загружаем массив сетей из базы:

my $in = $dbh->selectall_arrayref('select net, geo from locations order by net'));

Так же не забываем в начале объявить переменные:

my $last;

И открыть файл для записи.

open TARGET_BIG, ">", $file_name;

Отдаем все функции перебора сетей:

sub main_stack {
  my $IN = shift;
  my $S = [];
  my $item;
  my $LB = 0; # Крайняя левая граница
  for my $item ( @$IN ) {
    unless ( $item->[2] ) {
      my ( $ip, $mask ) = split /\//, $item->[0];
      my $st = str2dw( $ip ) & ( ( 2**$mask - 1 ) << ( 32 - $mask ) );
      $item->[2] = [ $st, $st + 2**( 32-$mask ) - 1 ];
    }
    while ( @$S ) {
      $S->[0][2][0] = $LB;
      my $c = split_ip( $S->[0], $item );
      # нет правой части.
      unless ( $c->[2] ) {
        shift @$S;
      }
      # Эта часть для сохранения в файл
      if ( $c->[0] ) {
        save_range( $c->[0] );
        $LB = $c->[0][2][1] + 1;
      }
      if ( $c->[1] ) {
        unshift @$S, $c->[1];
        last;
      }
    }
    unless ( @$S > 0 ) {
      unshift @$S, $item;
      $LB = $item->[2][0];
    }
  }
  # Ограничиваем слева оставшиеся данные из стэка
  # Дописываем содержимое стэка
  while (my $item = shift @$S) {
    save_range( [ undef, $item->[1], [ $LB, $item->[2][1] ] ] );
    $LB = $item->[2][1] + 1;
  }
  # Досохраняем последний элемент
  save_range( [ undef,0, [0, 0] ] );
}

Эта функция сохраняет диапазон

sub save_range {
  # Если пришел соседний диапазон с тем же id, то просто суммируем
  if ( $last ) {
    if ( $last->[1] eq $_[0]->[1] && $last->[2][1] + 1 == $_[0]->[2][0] ) {
      $last->[2][1] = $_[0]->[2][1];
    }
    print TARGET_BIG dw2str( $last->[2][0] ) . '-' . dw2str( $last->[2][1] ),
      "\t", $last->[1], ";\n" if $last->[1];
  }
  $last = [ undef, $_[0]->[1], [ $_[0]->[2][0], $_[0]->[2][1] ] ];
}

Для разбиения переданного диапазона используем следующий алгоритм

sub split_ip {
  my $c = [];
  if ( $_[1]->[2][0] > $_[0]->[2][1] ) {
    $c->[0] = [ undef, $_[0]->[1], $_[0]->[2] ];
  } else {
    $c->[0] = [ undef, $_[0]->[1], [ $_[0]->[2][0], $_[1]->[2][0] - 1 ] ]
      if $_[1]->[2][0] > $_[0]->[2][0];
    $c->[1] = [ undef, $_[1]->[1], $_[1]->[2] ];
    $c->[2] = [ undef, $_[0]->[1], [ $_[1]->[2][1] + 1, $_[0]->[2][1] ] ]
      if $_[1]->[2][1] < $_[0]->[2][1];
  }
  return $c;
}

Вспомогательные функции конвертирования ip

sub str2dw {
  return unpack 'N',pack( 'C4',split /\./, shift );
}

sub dw2str {
  return join '.', unpack 'C4', pack 'N',shift;
}

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