Pull to refresh

SSH2 в php5 + Mikrotik RouterOS, подводные камни

Reading time5 min
Views9.6K
Стояла задача: в цикле, из скрипта на php5 зайти по ssh на Mikrotik, сгенерировать скрипт с текущим конфигом, забрать скрипт на некое локальное хранилище. И так для ~500 роутеров. Так как в провайдерских кругах микротик весьма нередкий зверь — думаю кому-то ещё может пригодиться.

Так как глубоких познаний в тонкостях реализации ssh2 на микротике, в пхп, да и вообще — не имею, а сроки сильно ограничены, встретившиеся проблемы решал подручными средствами и инструментами, особо не заботясь об «элегантности».

В процессе обнаружилось следущее:
1. При использовании ssh2_exec необходимо(!) прочесть ответ, иначе команда не будет запущена (выкопал в коментах в мануалах по PHP, поверил на слово, благо был готовый пример для copy-paste). Если это так — подосреваю, что дело в буферизации.
2. Микротик почему-то переваривает только один запрос (ssh2_exec, ssh2_shell, etc.). Если после ssh2_exec'a запросить второй ssh2_exec или ssh2_shell, или ssh2_sftp — сессия как правило (но не всегда!) «залипает» в ожидании ответа (дольше 5 минут не ждал).
3. Если запросить ssh2_shell для получения интерактивного shell'a, дабы иметь возможность «пообщаться» посодержательнее — микротик естественно соглашается, но вне зависимости от передаваемого типа терминала весело и щедро спамит color-кодами (чего не происходит при вызове ssh2_exec), которые сама библиотека ssh2 естественно парсить и не пытается, передавая их дальше. Это превращает обработку «чата» в гораздо менее тривиальную задачу, чем хотелось бы.
4. В ssh2 библиотеке НЕТ функции ssh2_disconnect, можно конечно пропатчить библиотеку, написав свою, но…
5. Если пытаться забирать скрипт прямо с «терминала» — в нём нет метки конца скрипта. Таким образом не известно с той стороны что-то тормозит и продолжение следует, или же это и в самом деле всё.

update (из коментов):
6. ssh2_scp_recv упорно заявлял что ему не удаётся скопировать файл, когда работал с микротиком (даже если это первый и единственный запрос в этом соединении), при том, что с Ubuntu при тех же параметрах работал (менял только IP).
/update

Как решил:
1. В скрипте делаю fork()
2. В child'e — ssh2_exec с командой '/export file=current'. Если всё успешно — выхожу exit'ом с кодом 0, иначе — 1. Соединение с микротиком само закрывается при завершении работы дитя.
3. Лювлю результат от дитятки, если успешно — форкаю ещё одно дитя, на этот раз с заданием снова подсоединится и уже по sftp забрать свежесозданный файл со скриптом.
4. Обрабатываю результат от дитятки.

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

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

/* Notify the user if the server terminates the connection */
function my_ssh_disconnect($reason, $message, $language) {
  printf("Server disconnected with reason code [%d] and message: %s\n",
  $reason, $message);
}

function backup_mt($device) {
	
	if (!function_exists("ssh2_connect")) die("function ssh2_connect doesn't exist");

	$methods = array(
	  'kex' => 'diffie-hellman-group1-sha1',
	  'client_to_server' => array(
	  'crypt' => '3des-cbc',
	  'comp' => 'none'),
	  'server_to_client' => array(
	  'crypt' => 'aes256-cbc,aes192-cbc,aes128-cbc',
	  'comp' => 'none'));

	$callbacks = array('disconnect' => 'my_ssh_disconnect');

	// for a process, as there is no ssh2_disconnect funciton and we have to close connection between commands.
	$pid = pcntl_fork();
	if ($pid == -1) {
	    echo ('could not fork');
	    return;
	} else if ($pid) {
    	 // we are the parent
	     pcntl_wait($status); //Protect against Zombie children
	     if (!pcntl_wifexited ($status)) {
	     			echo "Child faled to exit normally.\n";
	     			return;
	     }
	     if (pcntl_wexitstatus($status) >0) {
	          		echo "Child reported failure. \n";
	     			return;
	     }     
	} else {

		// we are the child, we don't return, we just die when job's done

		echo "Trying to connect to mikrotik host ".$device['name']."(".$device['ip'].") via ssh on port 22\n";
		if(!($con = ssh2_connect($device['ip'], 22,$methods,$callbacks))){
		    echo "fail: unable to establish connection\n";
		    exit(1);
		} else {
			    // try to authenticate with username root, password secretpassword
		    	if(!ssh2_auth_password($con, $device['user'], $device['pass'])) {
		        	echo "fail: unable to authenticate\n";
		        	exit(1);
		    	} else {

				    	echo "Connected. Preparing configuration file.\n";
				        if (!($stream = ssh2_exec($con, "/export file=curcfg" ))) {
				            echo "fail: unable to execute command\n";
				            exit(1);
				        } else {
					            // collect returning data from command
					            stream_set_blocking($stream, true);
					            $data = "";
					            while ($buf = fread($stream,4096)) {
					                $data .= $buf;
					            }
					            fclose($stream);
					            // we don't need $data value for now, we just ignore it, but we have to retrieve it to avoid delays.
					           exit(0); // do not return, we're child, we don't want to continue main prorgam copy to execute.
						}
			}
		}
		
	} // end of child code
	
// give mt. time to save config and child to fully die, closing connections.
sleep(1);

// now fork another child to retrieve configuration. Make another connection for that.
$pid = pcntl_fork();
if ($pid == -1) {
     die('could not fork');
} else if ($pid) {
     // we are the parent
     pcntl_wait($status); //Protect against Zombie children
     if (!pcntl_wifexited ($status)) {
     			echo "Child faled to exit normally.\n";
     			return;
     }
     if (pcntl_wexitstatus($status) >0) {
          		echo "Child reported failure. \n";
     			return;
     }     
} else {
	
     // we are the child again, we should not return from this section, we die when job's done 

	echo "Trying to connect to mikrotik host ".$device['name']."(".$device['ip'].") for sftp on port 22\n";
	if(!($con = ssh2_connect($device['ip'], 22,$methods,$callbacks))){
	    	echo "fail: unable to establish connection\n";
	    	exit(1);
	} else {
		    // try to authenticate with username root, password secretpassword
		    if(!ssh2_auth_password($con, $device['user'], $device['pass'])) {
		        echo "fail: unable to authenticate\n";
		        exit(1);
		    } else {

		    		echo "Downloading configuration via sftp\n";
					$sftp = ssh2_sftp($con);

					echo "Got sftp handle.\n";
					$size = filesize("ssh2.sftp://$sftp/curcfg.rsc");
					echo "File size: $size\n";
			       
					$stream = fopen("ssh2.sftp://$sftp/curcfg.rsc", 'r');			
					if (! $stream) {
							echo "Could not open file /curcfg.rsc\n";
							exit(1);
					}
			        else {
			        	   echo "Reading file...";
					       $contents = '';
					       $read = 0;
					       $len = $size;
					       while ($read < $len && ($buf = fread($stream, $len - $read))) {
					          $read += strlen($buf);
					          $contents .= $buf;
					          echo strlen($buf).'B...';
					        }       
					        file_put_contents ('/tmp/'.$device['ip'],$contents);
					        @fclose($stream);
					        echo "done\n";       			        	
	        		}           
			        exit(0); // do not return, we're child, we don't want to continue main prorgam copy to execute.
			}
	}
}  // end of child code

}

...


и где-то в основном скрипте вызов:
$device=array('type'=>$type_id,'user'=>$tokens[80],'pass'=>$tokens[56],'name'=>$tokens[71],'ip'=>$ip);
if ($type_id == DEV_TYPE_MIKROTIK)  backup_mt($device);



Tags:
Hubs:
+5
Comments18

Articles