Archive for the ‘未分類’ Category

先日、上司が作ったpythonプログラムのグローバル変数を、外部からのプログラムによって書き換えたいという要求が起こった。

プログラム内でグローバル変数として確保されているメモリ領域を他のプログラムから直接書き換える、なんてことできるのだろうかと調べてみたら、プロセス間通信というのがそれらしいことがわかった。

しかしチラっと説明を見てみるとかなり面倒くさいことが発覚。
そりゃそうだ。ちょっと考えてみれば結構深いところを触らなければいけないことくらいはわかる。

そうして思いついたのは以下の2つ

  1. 環境変数を使ってプログラム間で変数の値を共有する
  2. 中継ファイルを一つ作る

1の方法では不可能だった。
Linux上のプログラムはOS全体の環境変数について読み込むことは出来ても書き換えることはできない。

プログラム上で書き込むことができるように見えても、それはそのプロセスにおいての環境変数が書き換わるだけで、他のプログラムから環境変数にアクセスすると、その値は書き換わってはいないのだった。

次に2の方法を試す。
これはまあ問題ないだろうと思いつつ実装してみると、やはり期待どおり動いた。

そこで友人のdyama氏(https://dyama.orgの運営者)からpipeを使ってはどうかというご提案をいただく。

pipeというのは文字通りfifoなpipeなわけだが、簡単に挙動を説明するとこんな感じ

#terminal A
$mkfifo pipe
$tail -f pipe

#terminal B
$echo "hoge" > pipe

とすると、ターミナルAの方でhogeという文字列が表示される

#terminal A
$tail -f pipe
hoge

この仕組を使って、2つのプログラム間での通信をさせて、外部プログラム(プログラムB)からコマンドを送り、それをpythonプログラム(プログラムA)で処理をするという形にした。

簡単な図にするとこう

スクリーンショット 2019 05 09 10 26 14

他プログラムから自プログラムの変数への介入をさせたいプログラムAは、もともとのメインループに加えてスレッドを実装し、一秒ごとにpipeファイルを読みにいく挙動を付け加えた。

さほどスピードが必要とされる処理ではなかったので一秒に一回で十分だった。

I/Oの時間すら惜しいスピードが求められる処理であれば、プロセス間通信を使う他ないのかなと思うが、ひとまずこれで十分に使えるものが出来たので満足です。

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

ラズパイのキットはこれが調子いいです。

Raspberry Pi Zero WH Starter Kit 8GB
ケイエスワイ
売り上げランキング: 48,720

ラズベリーパイ、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ドングル : Abit 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 relay-server.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

によって、はじめて接続するホストの場合でも警告(fingerprint云々)を出さないようにしたり、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接続するという選択もあります。
普通の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のmanページで、これは推奨されています。

この方法での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