null

Динамическая реконфигурация GREшных туннелей на Cisco

В одном проекте понадобилось держать GREшный туннель до нескольких удалённых точек.
Всё бы ничего, если бы не оказалось, что один из концов туннеля периодически меняет свой внешний IP.
На ум сразу пришёл Dynamic DNS, который и был успешно реализован.

Небольшой preface.
Удалённо менять настройки нашего Cisco роутера можно двумя путями: перезагружать конфиг по snmp/tftp и, собственно, через ssh.
Первый способ довольно уязвим и, учтя специфику проекта, был сразу отвергнут.
Рассмотрим принципы автоматического управления киской через SSH.
В качестве серверной системы используется debian 8.3.
Для начала, сгенерируем отдельную пару ключей

# ssh-keygen -t rsa

И положим ключи в /usr/local/dyndns/ssh_key.
Выполним настроку нашего узла.

username dyndns privilege 15 password it_s_very_secure_password_to_forget
ip ssh pubkey-chain
username dyndns
key-string
сюда построчно копируем ключ
exit
end

Затем, установим на сервер некоторые зависимости.

# apt-get install build-essential autoconf automake libtool gdb screen
# cpan install Data::Validate::IP HTTP::Daemon

Вообще, управлять SSH из stdin не столь тривиально, поэтому я решил написать небольшой Perl модуль, способный управлять GNU Screen.
Использование последнего приносит сразу некоторое количество плюшек (включая perl realtime debug), которые я оставлю за рамками этой статьи.
Далее, собственно, код модуля.

#!/usr/bin/perl
# made by: KorG

package gnuscreen;

use strict;
use v5.18;
use warnings;
no warnings 'experimental';
binmode STDOUT, ':utf8';

use Data::Validate::IP;

my $USAGE  = ($0 // '') . " tunnel_no new_dest";
my $KEY    = "/usr/local/dyndns/ssh_key";
my $USER   = "dyndns";
my $HOST   = "10.0.0.1";
my $SSH    = "/usr/bin/ssh -i $KEY -e none $USER\@$HOST";
my $SCREEN = "/usr/bin/screen";
my $S_NAME = "cisco";
my $WR_MEM = 0;

sub new {
   my $class = shift;
   my $args = shift;

   $KEY    = $args->{key}    // $KEY;
   $USER   = $args->{user}   // $USER;
   $HOST   = $args->{host}   // $HOST;
   $SSH    = $args->{ssh}    // $SSH;
   $SCREEN = $args->{screen} // $SCREEN;
   $S_NAME = $args->{s_name} // $S_NAME;
   $WR_MEM = $args->{wr_mem} // $WR_MEM;

   return bless {}, $class;
}

sub wr_mem {
   shift; # self
   return $WR_MEM = $_[0] // $WR_MEM;
}

sub DIE {
   die "@_" || $USAGE;
}

sub CMD($) {
   (my $stuff = $_[0]) =~ s/'/'"'"'/;
   `$SCREEN -S $S_NAME -X stuff \'$stuff\n\'`;
}

sub chdest($$) {
   my ($self, $tun, $ip) = @_;
   $tun = $tun // -1;
   $ip = $ip // -1;

   DIE "Invalid tunnel number" unless $tun > 0 && $tun < 32;

   DIE "Invalid IP format" unless is_ipv4($ip);

   DIE "Screen already exists" unless `$SCREEN -ls $S_NAME` =~ m{^no socket}is;

   `$SCREEN -c /dev/null -dmS $S_NAME $SSH`;

   sleep 1; # just wait for login

   CMD "conf t";
   CMD "int tun $tun";
   CMD "tun dest $ip";
   CMD "end";
   CMD "wr mem" if $WR_MEM;
   CMD "exit";
}

1;

Модуль позволяет выполнять команды в указанном скрине.
В нашем случае реализованы две функции: chdest -- изменяющая tunnel destination IP и wr_mem -- getter/setter переменной модуля, отвечающей за сохранение конфигурации в NVRAM. IP самой киски указан в переменной HOST.
Из кода видно, что легко масштабировать решение для любых других задач, будь то управление VLC, сервером Minecraft или типа того.
Код, реализующий сервер dyndns представлен ниже.

#!/usr/bin/perl
# made by: KorG

use strict;
use v5.18;
use warnings;
no warnings 'experimental';
binmode STDOUT, ':utf8';

use HTTP::Daemon;
use Data::Validate::IP;

use gnuscreen;

my %HOSTS = (
   host1.contoso.com => 1,
   host2.contoso.com => 2
);
my $cisco = gnuscreen->new();

for(;;) {
   my $srv = HTTP::Daemon->new(ReuseAddr => 1, LocalPort => 80) // die $!;
   while (my $conn = $srv->accept) {
      my $resp = HTTP::Response->new;

      while (my $req = $conn->get_request) {
         last unless $req->method eq 'GET';

         $conn->force_last_request;

         my %args = $req->url->query_form;

         my $ip = $args{myip} // '';
         my $host = $args{hostname} // '';

         $resp->content("nohost"), last unless defined $HOSTS{$host};

         $resp->content("badauth"), last unless is_ipv4($ip);

         $cisco->chdest($HOSTS{$host}, $ip);

         $resp->content("good $ip");
      }
      $conn->send_response($resp);
      $conn->send_crlf;
      $conn->close;
      undef $conn;
   }
}

В хеше HOSTS хранятся соответствия имён хостов и номеров туннелей.
Доменная часть имени будет подставляться из Cisco ip domain.
Естественно, можно обойтись и без домена, указав прямо в конфигурации желаемый hostname вместо <h>.
В общем случае, настройка удалённого узла следующая.

ip domain name contoso.com
ip ddns update method KorG
interval maximum 0 0 10 0
HTTP
add http://dyndns.contoso.com/?hostname=<h>&myip=<a>
exit
exit
interface GigabitEthernet0/1
ip ddns update hostname host1
ip ddns update KorG
end
wr mem

Максимальный интервал между обновлениями задается количеством дней, часов, минут и секунд.
В нашем случае -- не реже, чем раз в 10 минут. Чтобы изменения вступили в силу, необходимо передёрнуть интерфейс. Например shutdown/no shutdown, физически передернуть линк или перезагрузить маршрутизатор, если вы привыкли администрировать windows.
Отлично. Осталось лишь подружить наш сервер и systemd.
Вообще, было много вариантов запуска сервера: просто запускать как сервис, использовать daemon(1) и др.
В статье используется вариант, основанный на том же GNU Screen.
Напишем вот такой скрипт в /etc/init.d/dyndns

#!/bin/sh
### BEGIN INIT INFO
# Provides:          dyndns
# Required-Start:    $local_fs
# Required-Stop:
# Default-Start:     2 3 4 5
# Default-Stop:
# Short-Description: dyndns server for dynamic GRE reconfiguration
### END INIT INFO

PATH=/sbin:/bin:/usr/local/bin/:/usr/bin

case $1 in
   start)
      PERL5LIB=/usr/local/dyndns
      export PERL5LIB
      screen -c /dev/null -dmS dyndns /usr/local/dyndns/dyndns.pl
   ;;
   stop)
      screen -S dyndns -X quit
   ;;
   *)
      echo "Usage: $0 start|stop" >&2
      exit 2
   ;;
esac

Выполним ряд команд, чтобы его запустить:

# systemctl daemon-reload
# systemctl enable dyndns
# systemctl start dyndns

Готово. Теперь можно проверить работу сервера отправив строчку "GET /" на порт 80. Ответ представлен ниже.

# nc 0 80
GET /
nohost

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

korg

 

Коротко о себе

Работаю в компании Tune-IT, администрирую инфраструктуру компании и вычислительную сеть кафедры Вычислительной ТехникиСПбНИУ ИТМО.

Интересы: администрирование UNIX и UNIX-like систем и активного сетевого оборудования, написание shell- и perl-скриптов, изучение технологий глобальных сетей.
Люблю собирать GNU/Linux и FreeBSD, использовать тайлинговые оконные менеджеры и писать системный софт.