Archive for the ‘未分類’ Category

最近はラズベリーパイをつかってIoTデバイスを作ろうという試みをやっています。

ラズベリーパイ、Linuxが搭載されていますし、無線LANの挙動も実に軽快なので、LAN内にいる時はSSH接続がとても簡単です。しかし、IoTデバイスというのは常にLAN内にあるものとも限らず、SIMカードを使ってスマホのように各キャリアが提供するネットワークを使ってインターネットにアクセスする必要もあります。

私はラズパイをインターネットに接続させるSIMとして、Soracomを選びました。
SoracomのSIMにはグローバルIPが割り振られておりません。
そしてIPが割り当てられていない以上、一般的なTCP/IP - DNSを使ったSSH接続はできないということになります。

とはいえ開発中、Docomoネットワークを使ってインターネットに接続するSIMを使ったラズパイに対して、SSH接続したいことは往々にしてよく起こります。
それができなければわざわざシーケンス制御のプログラムを書いて実行しなければならない、といったような。

ということで、SoracomのSIMをはじめとする、グローバルIPが割り振られていないSIMを使ってインターネットにアクセスをしているラズパイに対して、SSH接続をする方法(NAT超えSSH接続)をここに記しておきたいと思います。

使っているラズパイの条件は下記の通りです。

raspberry pi : Raspbian GNU/Linux 9.8 (stretch)
autossh : 1.4e
soracom sim : SORACOM Air SIM plan-D (SMS/data)
3G USBドングル : AK-020

中継サーバーが必須

まずは中継サーバーを用意しましょう。
この方法は図のように、中継サーバーを介してラズパイにアクセスをすることになります。

スクリーンショット 2019 03 18 17 57 29

上にも書きましたがSoracom SIMを使ったラズパイの場合、グローバルIPが割当てられていないことで、通常のTCP/IPだけでは座標を特定することができないからです。

なので、SSHポートフォワーディング(SSHトンネル)という技術を使って、中継サーバーを介してラズパイにアクセスできるようにします。

簡単に順番に仕組みを説明すると

  1. ラズパイ、起動直後に中継サーバーに対してSSHポートフォワーディングをする
  2. Macbook、中継サーバーにSSHアクセスする
  3. 中継サーバーに入ったMacbook、ポートフォワーディング情報をもとに、ラズパイにアクセスする

ということです。

ポートフォワーディングというのは、SSH接続をする際に、接続先のサーバーの任意のポートと、接続元のデバイスの任意のポートを転送(同一化)させるというような技術です。

このケースの場合、(ラズパイのSSHポートは22番であるという前提とします)接続先の中継サーバーの4649番のポートを、接続元のラズパイの22番ポートに転送するということによって、中継サーバーにアクセスしたMacbookは中継サーバーの4649番ポートにssh接続をすることで、ラズパイに対してssh接続をするという仕組みを取ります。

実践

前提として中継サーバーに接続する際に使うユーザーを中継サーバー側で用意しておく必要があります。

このケースの例としてuserを使いますので、中継サーバー側で作成しておいてください

# adduser user

鍵の作成

この方法ではラズパイの起動直後に中継サーバーに対してSSH接続をしなければなりませんので、パスワードを入力することができません。そのため公開鍵暗号方式を使ってパスワードなしで接続できるようにします。

まずは、中継サーバー側のsshd_configのPubkeyAuthenticationyesに設定しておきましょう。

次にラズパイ側でssh接続のための鍵を作ります。

ssh-keygen -t rsa

(鍵の暗号化方式はどれがよいかという議論はありますが、ここでは省略、今回はrsaを採用します。)

あとは対話形式で鍵の作成が進んでいきますが、パスフレーズを聞かれるところでは何も入力せずに進んでください。

+---[RSA 2048]----+ 
|       ..  . ... |
|        ... o .  |
|        o. + * o |
|       . .. o & o|
|        S    = O |
|            ..+ o|
|          ..oooEo|
|           o*==+*|
|           .*@BO=|
+----[SHA256]-----+

こういう暗号のイメージが表示されたら鍵の作成は完了です。

~/.ssh/に
id_rsa 
id_rsa.pub
の2つのファイルが生成されています。

id_rsaが秘密鍵、id_rsa.pubが公開鍵です。
秘密鍵は絶対に人の手に渡らないように、ネットワークに流れることのないようにしましょう。

中継サーバーへ公開鍵の転送

出来上がった公開鍵を中継サーバーに送り、鍵を認証してもらいます。

ssh-copy-id user@192.168.0.100

#sshdのlistenポート番号を変更している場合は下記のように
ssh-copy-id user@192.168.2.30 -o port=10000

userと@以降のipについてはssh接続するユーザー名と接続先のホスト名またはIPアドレスを入力します。

コマンドが通ればパスワードを尋ねられますので、中継サーバー上でuserに設定しているパスワードを入力しましょう。公開鍵の転送が完了します。

この状態で

ssh user@192.168.0.100

と入力して、パスワードを聞かれずにサーバーにログインできれば成功です。

SSHポートフォワーディング

ラズパイから中継サーバーにログインする時に使うコマンドに、ポートフォワーディング用のオプションを追記します。

ssh -R 4649:localhost:22 user@192.168.0.100

-R 4649:localhost:22

というオプションを追加することで、中継サーバーの4649番ポートをラズパイの22番ポートに転送できます。

したがって上図に示しているMacbookは、中継サーバーにログインした後

ssh pi@localhost -o port=4649

とすることで、中継サーバーを経てラズパイにログインすることが可能になりました。

ラズパイから中継サーバーへssh接続の自動化

ラズパイの起動直後に中継サーバーへssh接続を自動化するには、ラズパイのcrontabに追記するのが良いのではないかと思います。

crontab -e

によってcrontabを開き、

@reboot ssh user@192.168.0.100

と書き込めば起動時に自動的に中継サーバーに接続しに行くようになります。

このとき、いくつかの設定を追加するのも良いかと思います。
具体的には ~/.ssh/config に下記のような設定を追加しておきます。

Host RelayServer
  HostName RelayServer.com #中継サーバーのホスト名
  User user
  IdentityFile ~/.ssh/id_rsa
  
  StrictHostKeyChecking no
  UserKnownHostsFile /dev/null
  ServerAliveInterval 60
  ServerAliveCountMax 3
  ExitOnForwardFailure yes
  RemoteForward 4649 localhost:22

それぞれのオプションの説明をします。

HostNameは中継サーバーのアドレスです。
Userには中継サーバーにログインするユーザー名を入力します。
IdentityFileは秘密鍵の場所を書きます。

StrictHostKeyChecking no
UserKnownHostsFile /dev/null

によって、はじめて接続するホストの場合でも警告などを出さないようにしたり、known_hostsファイルに影響を与えることなく接続できます。

ServerAliveInterval 60
ServerAliveCountMax 3

このオプションによって、サーバーからのタイムアウトを回避します。
60秒毎にサーバーにメッセージを送り、ServerAliveCountMax の設定回数、3回まで繰りかえします。
もしサーバーが応答しなくなったら 180秒後に接続が切断されるようになるということですね。

ExitOnForwardFailure yes

このオプションによって、既にポートフォワード先のポートが空いてなかった場合はssh接続をせずに終了します。

RemoteForward 4649 localhost:22

このオプションで接続先の中継サーバーの4649番ポートに、接続元のラズパイの22番ポートに転送できます。

これらの設定が終われば、

ssh RelayServer

と入力するだけで上記で設定したオプションを踏まえたssh接続ができます。

自動で接続する際には -N -f オプションをつけてもいいです。それぞれ

-N は接続先でポートフォワーディングのみに利用する接続であることを明示するオプション

-f はバックグラウンドで実行することを明示するオプションです。

crontabの方にも
@reboot ssh -N -f RelayServer

と書いておくとこれらが自動で実行されます。

autosshのインストール

さらにautosshをインストールするという選択もあります。
普通のsshを使っても問題はないのですが、autosshの場合、セッションが何らかの原因で途切れてしまった場合、自動的に再接続をしてくれます。

autosshはsshと互換のあるコマンドですので、普段からautosshを使うのもありですね。

#apt-get install autossh

でインストールできます。

上記のことをautosshでやる場合、crontabには

@reboot autossh -M 0 -N -f RelayServer

と書きましょう。

-M 0 オプションは
autossh標準のsshコネクションをモニタリングを無効にし、OpenSsh自体のモニタリングを有効にします。
OpenSsh自体のモニタリングとはServerAliveInterval 60や、ServerAliveCountMax 3などの設定のことです。
autossh公式マニュアルで、これは推奨されています。

この方法でのSSH接続は信頼性があまり高くない

以上のことを実行すると、ラズパイ起動時に中継サーバーに自動で接続しにいき、そのセッションを使ってクライアント(macbook)からラズパイに接続しにいくことができるようになります。

しかし、ラズパイを再起動などすると、再接続までにとても時間がかかったりします(時間図ったら10分くらいかかりました)
その場合、中継サーバーをリブートしてから、ラズパイをリブートすると、すぐに接続できたりするのですが…

おそらく、ラズパイリブートする前にautosshの接続をきちんと切ってからリブートすればいいのかなあなんて今書きながら思いましたが、まだ試していません。

いずれにしろ、この方法を使ったSSH接続はあくまで開発途中での実験やデバッグに用いるのがよさそうです。

中継サーバー上のポートListenの履歴を、ラズパイが沈黙した後にすぐに削除するなどの施策を取るなどができれば完全に安定させることができるのかも知れませんが。

なお、この記事を書くにあたって下記のサイトを参考にさせていただきました。
ありがとうございます。

SSH公開鍵認証でパスワード無しでログインする!ユーザも指定できる! | ぴぐろぐ

Soracom Airで繋がったデバイスにリモートからSSHする | DevelopersIO

OpenSSHの警告メッセージを出さないようにする方法 - Qiita

autossh(1): monitor/restart ssh sessions - Linux man page

node.jsを新しいプロジェクトに採用するにあたって、おさらい的な意味で入門書を読みかえしている。

とても良い本なので、nodejsをこれから始めようと思っている皆さんにもおすすめしたい。

この本の中のsocket.ioに関する項目について、実装してみたところ不具合が発生したので、自分の覚書として記しておきたい。

実行環境は以下の通り
sakuraVPS CentOS Linux release 7.5.1804 (Core)
nodejsのバージョンは v10.0.0
npmからインストールしたsocket.ioのバージョンはv1.7.4
クライアント側のsocket.ioはcdnよりv2.2.0

サーバー側


//io_server.js

var http = require('http');
var fs = require('fs');
var Io = require('socket.io');

var server = http.createServer(function(req,res){
  var source = fs.createReadStream('index.html');
  res.writeHead(200);
  source.pipe(res);
})

io = Io(server);
let counter=0;

io.sockets.on('connection',function(socket){
  io.emit('change',{
    count:counter+1
  });

  socket.on('join',function(){
    counter++;
    socket.broadcast.emit('change',{
      count: counter
    });
  });
  
  socket.on('disconnect',function(){
    counter--;
    socket.broadcast.emit('change',{
      count:counter
    });
  });

});

server.listen(3000);

クライアント側


<!-- index.html -->

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>chatroom</title>
</head>
<body>
<h1>現在の人数</h1>
<h2 id="counter">0人</h2>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js"></script>

<script>
//var socket = io.connect("localhost:3000");
var socket = io.connect();

socket.on('change',function(o){
  document.getElementById("counter").innerHTML = o.count+ "人";
});

socket.on('connect',function(){
  socket.emit('join');
});

</script>
</html>

書籍では、index.htmlの16行目は
var socket = io.connect("localhost:3000");
とあるが、これだと動作しなかった。
結果、その下の行の
var socket = io.connect();
で動作することを確認したので記しておく。

ブラウザのコンソールで発生するエラーをもとにググってたどり着いたのが以下のページ

javascript - SocketIO ERR_CONNECTION_REFUSED - Stack Overflow

このページを見ることで解決できたのだけれど、socket.ioを0.9から1.3.xにアップグレードしたあとio.connectには引数を使うとエラーが起こるようになったらしい。

socket.io公式のチュートリアルにおいても同じ内容で表記してある。

ブラウザのタブを開く数(閲覧者の数)に応じて、ブラウザ内の人数の値が変化する。

スクリーンショット 2018 12 06 11 20 51

あるディレクトリの中のディレクトリ群を、全て個別のzipアーカイブにしたいと思った。

そこで

for i in `ls`; do zip -r $i.zip $i; done;

というコードを書いて実行したところ、すべてのディレクトリの中に「.zip」というファイルが出来上がってしまった。

いやいや、もう一つ上のディレクトリに、個別のディレクトリと個別のディレクトリのzipアーカイブが並んでほしかったのだけど。

何がおかしいのだろうと思って

for i in `ls` ; echo $i.zip

を実行したところ


dir1/.zip
dir2/.zip

という結果が出てきた。

つまり、ディレクトリをあらわす記号"/"の後に.zipがついていたわけです。
なるほど、自分のzshはディレクトリの名前をタイプするだけでそのディレクトリの中に入るような仕様にカスタムしているので、

cd dir1

をしていることと同様になり、その後.zipを作っているということになっていたわけです。

ということでディレクトリの末尾のスラッシュを消す方法を探したところ、以下のコマンドで達成できました。

for i in `ls` ; echo ${i%/}.zip

${i%/} %/を変数に対して付加してあげると、末尾のスラッシュを取り除いてくれるようです。

これは例えばスラッシュでなくても、i%#とすれば末尾のシャープを取り除いてくれるようですね。

結果、以下のコードを実行することで思っていたことが出来ました。

for i in `ls` ; do zip -r ${i%/}.zip $i; done;

勉強になりました。