Давно хотел применить на практике возможности Cisco IOS, которые прячутся за командой tclsh и присутствуют практически в каждом маршрутизаторе и коммутаторе. Но к сожалению, а может к счастью не приходилось решать задачи где использование автоматизации средствами самого устройства могло хоть как-то помочь, впрочем и устройств от Cisco под моим управлением никогда не было очень много. Наконец, судьба закинула меня в командировку откуда надо было управлять сетью, а в руках только планшетник с Wi-Fi и 80-й TCP порт. В этот раз пришлось надиктовывать команды голосом через телефон, но по приезду задача была решена с использованием Cisco IOS Scripting with Tcl. К счастью реализация TCL в Cisco IOS достаточно функциональна чтобы решить такую задачу.
Сначала я отправился на поиски готового решения. Собственно сама Cisco предлагает доступ к своим устройствам через web-интерфейс, но такой доступ не обладает полностью всем возможностями консольного интерфейса (как указывает сама Cisco), требует предварительной настройки (например, Java для SDM), влияет на безопасность, не удобно для использования (для меня) — в общем на всех устройствах no ip http server. Также можно было замапить порт SSH/telnet на HTTP, но вот только на планшетнике с этим проблема: telnet уже и в Windows сразу давно нету, а уж ssh. Примем во внимание, что нам может потребоваться зайти максимально быстро с любого устройства где есть web-браузер, пусть это будет телефон начала/середины 2000-х.
Смотрим на сторонние готовые решения удалённого доступа к консоли Cisco — вот наверное самая популярная статья на эту тему, но вспоминаем что нам нужен как минимум telnet. В итоге, сформируем требования: доступ через web-интерфейс, максимально просто запускать на устройстве (идеально одной командой) — и напишем всё сами.
Сначала то что получилось — по ссылке в конце поста можно забрать скрипт, который должен запускаться на удалённом устройстве. Ввиду того что реализация TCL есть для очень многих систем, поэтому и доступ можно получить к Cisco IOS, Windows, Linux, FreeBSD (это только то что я проверил сам). В параметрах надо указать адрес на котором будем слушать входящие запросы, и порт. Если параметры не указывать то слушаем все адреса и первый свободный порт выделенный нам системой. Есть подобие подсказки (как в Cisco) когда в параметрах попадается знак вопроса, ирония в том что в консоли Cisco подсказка не работает, знак вопроса перехватывает сама консоль Cisco. Первая строчка не содержит стандартного #!<путь до скрипта>, поэтому явно вызываем tclsh (для Cisco по другому и нельзя):
win>tclsh cws.tcl ? A.B.C.D or * Listen ip address <cr> win>tclsh cws.tcl * ? <0-65535> Listen tcp port, 0 for first free <cr> win>tclsh cws.tcl * 8181 Listen on http://0.0.0.0:8181
Для консоли Cisco пишем полный путь (можно через tftp), при этом надо быть в привилегированном режиме. В режиме tclsh вызов скрипта через source, но параметры командной строки нельзя будет передать в скрипт:
cisco#tclsh tftp://192.0.2.1/cws.tcl * 8181 Listen on http://0.0.0.0:8181 cisco#tclsh cisco(tcl)#source tftp://192.0.2.1/cws.tcl Listen on http://0.0.0.0:43012
Для nix консолей параметр "*" воспринимается как шаблонный параметр (wildcard) поэтому надо его экранировать "*" либо заключать в кавычки.
После запуска можно заходить браузером по прослушиваемому адресу. Интерфейс аскетичный — строчка для ввода команд, кнопки выполнения и остановки скрипта, также переключатель режима tclsh.
Если выполняемой команде есть что вывести, то вначале страницы будет показан вывод команды. Режим tclsh выполняет команду как скрипт TCL — можно использовать весь набор команд языка. Поле для ввода ограничено 160 символами, размер 40 символов (удобно для планшетника). Кнопка остановки скрипта завершит его работу на устройстве, нам не надо будет просить об этом кого бы то ни было когда мы закончим наши действия. Ошибки выполнения самой команды обрабатываются и выводятся обратно в web-интерфейс, ошибки связи и скрипта никак не отлавливаются, так что в консоли устройства могут вылезти стандартные ошибки интерпретатора TCL (постарался максимально всё отловить, но может чего и осталось). Стоит также помнить что за раз скрипт отрабатывает только один запрос, и если выполнить команду которая требует интерактивных действий или длительного периода выполнения, тогда доступ для дальнейшего управления скриптом будет потерян. Можно завершить выполняемую команду символом "&" через пробел, что запустит её в фоновом режиме и вернёт управление обратно скрипту, не работает для Cisco IOS.
Главная особенность режима tclsh на Cisco состоит в том, что все команды Cisco воспринимаются интерпретатором как родные, то есть фактически мы дополняем язык TCL всем тем функционалом что имеет конкретный командный интерфейс устройства — нам не нужно явно вызывать команды с помощью exec, они и так уже выполняются. Также мы не сможем перейти в режим конфигурации с терминала conf t, для этого надо использовать команду дополнение TCL от Cisco — ios_config. Например, выключить интерфейс:
cisco#conf t cisco(config)#int fa0/0 cisco(config-if)#shutdown cisco#tclsh cisco(tcl)#ios_config "int fa 0/0" "shutdown"
Очень подробно об этом как всегда на cisco.com.
Напротив, в Windows все команды не выполняются в консоли, то есть воспринимаются только в виде отдельного исполняемого файла, поэтому чтобы выполнить, например, dir надо явно вызвать консоль:
win>cmd /c dir c: Том в устройстве C имеет метку SYSTEM Серийный номер тома: A073-3CE1 Содержимое папки c:
С функционалом закончили, теперь немного про реализацию. Чтобы принимать TCP соединения используем команду socket в режиме сервера, ключ -server. При необходимости передаём адрес для прослушивания (если его не будет то прослушиваться будут все адреса) ключ -myaddr. Обязательным параметрам является имя callback процедуры которая будет вызвана при передаче данных в установленном соединении, в нашем случае это get_http. Как я уже писал, ошибки при создании соединения не проверяются, если что-то пойдёт не так скрипт будет ругаться в консоль. Открытый сокет сохраняется в переменную wsh, из которой мы получаем его параметры командой fconfigure $wsh -sockname — прослушиваемый адрес и порт, чтобы отобразить их на экран.
if { $argc == $i } then { if [string length $listenaddr] then { set wsh [socket -server get_http -myaddr $listenaddr $listenport] } else { set wsh [socket -server get_http $listenport] } set sockparam [fconfigure $wsh -sockname] puts "Listen on http://[lindex $sockparam 0]:[lindex $sockparam 2]" after $connwait set stopsrv 1 vwait stopsrv close $wsh set retcode $stopsrv } else { set retcode [expr $i + 100] } return $retcode
Затем вызываем vwait которая ждёт изменения переменной stopsrv . Без данной команды callback процедура не будет принимать соединения, сервер как таковой вообще не будет прослушивать порт. Фактически только после вызова данной команды мы переходим в режим сервера: TCL уходит во внутренний цикл где и обрабатываются запросы на соединения. Переменная stopsrv нужна для того чтобы прекратить обработку соединений — остановить сервер, мы её установим при нажатии на кнопку остановки в web-интерфейсе — TCL выйдет из цикла прослушивания, завершит callback процедуру get_http и только потом выполнить команду close, идущую за vwait, которая закроет открытый сокет. Перед vwait, защитный механизм: если мы передумали заходить или долго не выплоняем никаких действий, то по истечении времени заданного в переменной connwait = 15 минут, stopsrv будет установлена и vwait продолжит выполнение. Если мы зашли то after переинициализируется заново в процедуре get_http.
Обёртка из if нужна для корректной передачи параметров командной строки в данный блок, сами параметры проверяются ранее, что там да как можно посмотреть в самом скрипте.
Помним об особенности Cisco tclsh, что команды Cisco для него как родные, поэтому вместо exit которая относится именно к Cisco а не TCL, используем return — после выполнения скрипта на Cisco, в консоль будет выдан код завершения.
Вся основная работа выполняется в процедуре get_http, где анализируются входные параметры HTTP запросов и вызываются процедуры для формирования HTTP ответов:
proc get_http { sockaddr ipaddr portaddr } { global stopsrv global connwait gets $sockaddr r flush $sockaddr set rs [string tolower [string trim $r]] after cancel set stopsrv 2 after $connwait set stopsrv 2 switch -regexp -- $rs { {^gets*/close} { set body 1; set stopsrv 0 } {^gets*/s+} { puts "GET from $ipaddr:$portaddr"; after cancel set stopsrv 1 } {^gets*/?cmd=} { if { [regexp {/?cmd=([^[:space:]^&]*)(&tclsh)?} $r opt cmdline checked] && [string length $cmdline] } then { set cmdline [expandPercent $cmdline] if { ! [string length $checked] } then { catch "exec $cmdline" msgout } else { catch $cmdline msgout set checked {checked} } } } default {set mode 1; set body 2 } } response_http $sockaddr $mode response_html $sockaddr $body $cmdline $msgout $checked close $sockaddr }
В качестве параметров данной процедуры выступают: открытый сокет sockaddr, откуда мы будем читать и куда будем писать, а также порт portaddr и адрес ipaddr присоединившегося клиента. Определяем глобальную переменную stopsrv чтобы увидеть её вне данной процедуры, нужна для vwait. Читаем только первую строчку (остальные нам не нужны — flush), ожидаем что будет запрос GET. Проверяем это в switch и действуем в соответствии с тем что нам передаёт клиент. Если мы чего то не знаем, то вернём «HTTP/1.0 501» (не можем отобразить запрашиваемое содержимое). На верный запрос мы отвечаем «HTTP/1.0 200».
Все ответы HTTP формируются в процедуре response_http, а HTML страничка в response_html. Я не буду описывать эти процедуры, в них линейный код вывода разметки страницы — просто несколько строчек puts $sockaddr <текст> и проверки условий что именно выводить.
Реагируем на следующие запрашиваемые данные:
- / — корень — выводим нашу страничку, а в консоль устройства пишем кто к нам зашёл — адрес и порт. Также отменяем паузу after;
- /close — завершаем работу скрипта — выставляем переменную stopsrv (на самом деле от клиента прилетает "/close?", как запрос с формы без параметров);
- /cmd=<команда>&tclsh — сначала раскроем текст из HTTP запроса — к нам прилетает обработанная строчка где все пробелы заменены на "+", все нестандартные символы (почти все символы кроме букв и цифр) представлены в виде %XX. Для этого мы используем expandPercent, практически неизменно с Wiki TCL. Далее очищенную команду выполняем через catch и результаты выполнения сохраняем в msgout, для вывода на нашей страничке. Вызывая команду таким образом мы отлавливаем возможные ошибки её выполнения. Если присутствует параметр «tclsh» то выполняем как есть, если не присутствует то выполняем через exec — что даёт нам эффект выполнения команды в консоли устройства.
Сразу после того как мы отправляем ответы, сокет закрываем. То что мы не поддерживаем «keep-alive» соединения сообщаем в каждом HTTP ответе опцией «Connection: close» и указывая версию «HTTP/1.0». Скрипт никоим образом не пытается соответствовать стандартам, я сделал минимально возможную обработку, чтобы те браузеры которые были под рукой (Opera 12, IE7, Chrome) реагировали на передаваемые данные нормально, в тех что не было под рукой могут быть сюрпризы.
Особенность синтаксиса языка непосредственно на Cisco IOS связана в большей степени с тем, что выполнена реализация TCL версии 8.3.4, а последняя версия 8.5.12. Например, удобные опции switch, -matchvar и -nocase не реализованы. В любом случае писать можно на любой платформе, просто строже относится к синтаксису и тогда проблем с переносом не будет.
В коде мог немного напортачить, где-то переборщил со стилем, где-то наоборот, на каких-то устройствах может не завестись в виду особенностей этих устройств, но в целом у меня выглядело довольно стабильно. Главное при использовании помнить что это всего лишь инструмент, а уже как к нему приложить голову зависит только от того кто эту голову прикладывает.
Скрипт можно забрать по ссылке — cws.tcl
TCL на Cisco — www.cisco.com/en/US/docs/ios/12_3t/12_3t2/feature/guide/gt_tcl.html
Команды TCL — www.tcl.tk/man/tcl8.5/TclCmd/contents.htm
Автор: Loiqig