📜 ⬆️ ⬇️

SSH2 in php5 + Mikrotik RouterOS, pitfalls

There was a task: in a loop, from php5 to go via ssh on Mikrotik, generate a script with the current config, pick up the script for some local storage. And so for ~ 500 routers. Since in the provider circles of microscopes a very frequent beast - I think someone else can come in handy.

Since I do not have deep knowledge of the intricacies of ssh2 implementation on microtics, in PCPs, and in general, and the terms are very limited, I solved the problems with improvised tools and tools, without particularly worrying about "elegance."

The process revealed the following:
1. When using ssh2_exec, it is necessary (!) To read the answer, otherwise the command will not be started (dug in comments in PHP manuals, took a word, since there was a good example for copy-paste). If this is so, I suspect that it is a matter of buffering.
2. For some reason, Mikrotik digests only one request (ssh2_exec, ssh2_shell, etc.). If after ssh2_exec'a request a second ssh2_exec or ssh2_shell, or ssh2_sftp, the session usually (but not always!) “Sticks” waiting for a response (I did not wait for more than 5 minutes).
3. If you request ssh2_shell to get an interactive shell, in order to be able to “communicate” more thoughtfully, the microtic naturally agrees, but regardless of the type of terminal being transferred, fun and generously spam color codes (which does not happen when ssh2_exec is called), which the library itself ssh2 naturally parses and does not attempt to pass them on. This makes the processing of "chat" in a much less trivial task than we would like.
4. In the ssh2 library there is NO ssh2_disconnect function, you can of course patch the library by writing your own, but ...
5. If you try to pick up the script directly from the "terminal" - there is no end of script tag. Thus, it is not known from the other side that something is slowing down and should be continued, or it really is all.
')
update (from comments):
6. ssh2_scp_recv stubbornly stated that he could not copy the file when working with microtic (even if it was the first and only request in this connection), despite the fact that it worked with Ubuntu with the same parameters (changed only IP).
/ update

How decided:
1. In the script, do fork ()
2. In child'e, ssh2_exec with the command '/ export file = current'. If everything is successful, I exit exit with code 0, otherwise - 1. The connection with the micro-switch itself closes upon completion of the child's work.
3. To love the result from the child, if successful - I will fork one more child, this time it will reconnect with the task and take the newly created script file via sftp.
4. I process the result from the child.

Thus, although it is crooked, all the above-mentioned problems are bypassed.

Below is a piece of code that implements this functionality, so that if anyone needs it, do not waste time creating a bicycle.

/* 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 } ... 


and somewhere in the main script call:
 $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); 


Source: https://habr.com/ru/post/110220/


All Articles