LoginSignup
5
3

More than 5 years have passed since last update.

Node.js でポートを監視し、結果を Slack に投稿する

Posted at

動機

サーバで何かが起こればシステム管理者にメールで通知するのが一般的?かと思いますが、家庭内サーバで起きるイベントは家族で共有したいものもあります。イベントの内容を家庭用Slackチームに投稿できれば便利だと思い立ちました。

まず今回は、特定のホストの特定のポートに接続し、接続できなかったら(何か異常があるとして)Slackに投稿してみます。

Node.jsを動かすDockerコンテナを作成する

スクリプト類をNode.jsで記述する前提で、公式リポジトリのnodeイメージを利用してDockerコンテナを作成します。npmで監視と投稿に必要なパッケージをインストールし、Slackの incoming webhook のURIを環境変数として渡しておきます。

Dockerfile
FROM node:latest
MAINTAINER flny flny@example.com

RUN npm install node-netcat \
                slack-node \
                request

ENV WEBHOOK_URI https://hooks.slack.com/services/xxxxxx...

CMD /bin/bash

node.jsで記述するスクリプトはデータボリュームコンテナ(scripts:)に置いて永続化することにしました。

docker-compose.yml
monitor:
  image: monitor
  container_name: monitor
  tty: true
  volumes:
    - /mnt/pub:/mnt/pub
  volumes_from:
    - scripts
  command: "/bin/bash"

scripts:
  image: busybox
  container_name: scripts
  tty: true
  volumes:
    - /mnt/scripts
$ docker build -t monitor <Dockerfileの場所>
$ docker-compose up -d monitor
Creating monitor

ポート監視の結果をファイルに書き出す

  • ポートスキャンの結果がNGだったら そのレスポンスをファイルに書き込む
  • ファイルの内容を読んでSlackに投稿する

の2つにスクリプトを分けました。

前者のscanPort.jsでは、node-netcatのportscanで var portsで指定したポートをスキャンしています。node-netcatのportscanは 必ずtcp/udp両方スキャンするので、udpでエラーが出たときにはそれを無視するようにしています。1
tcpのスキャンでエラーが出たら、system_port_<unix時間>というファイル名でエラーのレスポンス内容を保存します。2

scanPort.js
#!/usr/bin/env node

var host = 'example.com';
var ports = ['22', '53', '445', '8200', '80'];
var udpErrorStr = 'protocol \"udp\" with message';
var msgPath = "/mnt/pub/misc/monitor/"

var scanner = require('node-netcat').portscan();
var fs = require('fs');

var msgFile = msgPath + "system_port_" + (new Date()).getTime();

ports.forEach(function(port) {
  scanner.run(host, port, function(err, res) {
    if(err && err.indexOf(udpErrorStr) == -1) {
      fs.appendFile(msgFile, err + "\n");
    }
  });
});

ファイルの内容を読んで Slackに投稿する

scanPort.jsがファイルに出力したエラーの内容を、sendSlack.jsがSlackに投稿します。

  • 特定のディレクトリ(/mnt/pub/misc/monitor/)にある ファイル名が"system_port_" で始まるファイルを列挙する
  • ファイルの中身を読んで webhookのデフォルトチャネルに :warning: アイコンで投稿する
  • うまくいったら ファイルを投稿済みディレクトリ(/mnt/pub/misc/monitor/done/)に移動する

という流れです。slack-nodeのおかげで投稿部分はシンプルに記述できますが、全体として可読性が今ひとつ...。js力が低いのでもっとがんばる必要がありますね。

sendSlack.js
#!/usr/bin/env node

var msgPath = "/mnt/pub/misc/monitor/"
var donePath = "/mnt/pub/misc/monitor/done/"
var filePrefix = "system_port_"

var SlackNode = require('slack-node');
var slack = new SlackNode();
slack.setWebhook(process.env.WEBHOOK_URI);
var fs = require('fs');

fs.readdir(msgPath, function(err, files) {
  if(err) throw err;
  files.filter(function(file) {
    return fs.statSync(msgPath + file).isFile() &&
             file.startsWith(filePrefix);
  }).forEach(function (file) {
    fs.readFile(msgPath + file, 'utf8', function(err, data) {
      if(err) {
        throw err;
      }else{
        slack.webhook({
          icon_emoji: ":warning:",
          text: data
        }, function(err, res) {
          if(err) {
            throw err;
          }else{
            console.log(res);
            fs.renameSync(msgPath + file, donePath + file);
          }
        })
      }
    })
  })
});

結果を確認する

コンテナ上でスクリプトを実行します。80番ポートは開けてないのでポートスキャンがエラーになり、投稿されるはずです。

$ /mnt/scripts/scanPort.js
$ /mnt/scripts/sendSlack.js
{ status: 'ok',
  statusCode: 200,
  headers:
   { 'content-type': 'text/html',
     'transfer-encoding': 'chunked',
()
  response: 'ok' }

slack.png

うまく投稿できました!
あとはcronか何かでこれらのスクリプトを定期的に呼び出せば完成です。

以上、次回は別の何かを監視対象に加えてみようと思います。


  1. つまり、udpは監視してません 

  2. ファイル名をゆるく一意にしたいだけで、unix時間じゃなくてもいいかもです 

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3