Docker + Linuxでいい感じに自宅・小規模オフィス用ルータを作る

IMG_1139 2.jpg

年末年始になると自宅のネットワーク周りをいじりたくなるmizutaniです.1年くらい前にミラーリングできるスイッチを格安で手に入れてはしゃいで自宅ネットワークの監視環境を作ったんですが,今見直してみるとわりと複雑な構成で,これをどうにか整理できないかと昨年末に思い立ちました.機器の構成はなるべくシンプルにするとともにどうせなら今風な作りにしようということで,Docker + Linuxで構成するPCルータを作ってみました.

設計

原則

サービスのモジュール化

Linux kernelにやらせなければならない仕事を除き,各サービスをなるべく独立して動かせるようにします.

  • 市販のブロードバンドルーターなどと比べ,Linuxを入れたマシンは非常に自由度が高いためなんでもできますが,そのために環境が"汚れて"しまうという問題が有ります
    • 細かい変更を続けるうちにサービスや保存してあるファイルの依存関係などが徐々に分からなくなってしまいます
    • 特に新しいサービスを入れたり消したりということを試しているとゴミが残ってしまうようになります
  • ルータを構築する機会はそれほど多くはありませんが,それでもマシンの置き換えなどの際になるべく容易に移行できるよう,サービスとOSを分離させた構成が望ましいです

ランニングコストを安くする

以前と比べて運用に便利な外部サービス(SaaS等)も多くなりました.こういったものも積極的に利用していきたいですが,あくまで自宅や小規模オフィスを想定しているためあまり日々のランニングコストを掛けることは想定しません.

具体的には月額が数百円程度に収まることを目標とします.お金をだせばもっと良いサービスを色々使えるのは重々承知ですが,このポリシーに基づいて必要最小限の機能となるように設計しています.

エンジニア向けだと割り切る

そもそもルータを自分で作ろうというのはITエンジニアぐらいのものだと思いますが,あえて言うならエンジニアが自分でいろいろいじりたいという要望を叶えるのが目的です.したがって,既存のブロードバンドルーターなどを置き換える目的では設計していません.通常の目的であればブロードバンドルーターを買って設定するほうが遥かに安価で簡単です.

前提

  • ハードウェア想定
    • x86マシン(Raspberry Pi で構成するのもいいが,ちょっと性能に不安がある & ARMで頑張る気力はなかった)
    • NICを2つ以上つんでいる
    • RAMはある程度余裕を持って使える(4GB程度を想定)
    • HDDはあまり大きくなく,あまり恒久的なデータを残さない想定
  • 最低限提供するサービス
    • パケットフォワーディング (home network -> Internet)
    • ファイアウォール
    • DNS キャッシュサーバ
    • DHCP サーバ
    • 通信のモニタリング

構成

以上の要件などを踏まえ,以下の図のように設計しました.

home-router-2018-arch (1).png

  • パケットフォワーディング,ファイアウォールなどネットワークに直接関わる部分は素直にLinux OSにやらせる
  • それ以外のサービスは基本的にDockerで構成することでモジュール化し,サービスの追加・入れ替え,削除がやりやすいようにする
    • DNS キャッシュサーバ (unbound)
    • DHCP サーバ (kea)
    • 通信のモニタリング (softflowd, dns-gazer)
  • 適宜外部サービスを利用する
    • メトリクス監視 (mackerel)
    • ログ保存 (AWS S3)

実装

マシンの準備

今回用意したマシンは以下になります.

XCY Intel Celeron J1900ベアボーン(2Ghz クアッドコア 4スレッド) ギガビットLAN*4 ファンレス 小型 省スペース (4G RAM 32G SSD)
https://www.amazon.co.jp/gp/product/B06X16NPVQ
519cyUoLI+L._SL1000_.jpg

EthernetのNICが4つついており,かつCPUが2GHzクアッドコア,4Gメモリ,32G SSDと,ルータとして使うには十分な性能になっています.NICが4つあるのでセグメントを分けて遊ぶこともできますし,機体も小さいのでルータとして設置するのに適しています.

もちろんこのマシンではなくても,古いデスクトップPCにNICをもう一枚刺して使う,というような構成もありです.今回はこのXCYを使ってインストールすることを前提に解説します.

ホストとして使うLinuxのインストール

IMG_0024 2.jpg

OSのインストール

今回使ったXCYの機体は普通のx86マシンなので,PCやサーバにインストールするのと同様の手順で問題ありません.写真の通りUSBの口が2つついているので,そこにCD/DVDないしUSBメモリなどをさしてインストールをします.試していませんがCOMシリアルポートがついているので,それを使ってもインストール作業できそうです.

今回私はCD/DVDドライブを使い,もう1つのUSBにキーボードを繋げ,背面のVGA出力を画面に写しながらインストール作業をしました.USBのCD/DVDドライブを使う場合,BIOSのboot priorityをいじる必要がある…はずです(ちょっといろいろいじりながらやっていたので記憶が曖昧ですが)起動直後,ESCキーでBIOSに入れます.

IMG_0027 2.jpg

また,今回はホストOSとしてubuntu Linux 16.04を選択しました.以降,それを前提に解説します.

パッケージのインストール

$ sudo apt update
$ sudo apt upgrade -y
$ sudo apt install -y iptables docker.io docker-compose pppoeconf git

ホストOS側はなるべく余計なものは入れたくないので,導入パッケージはなるべく最小にしています.

インターフェースの設定

設計の図にもある通り,1つのNICをインターネット側に,もう1つのNICを内部ネットワーク側に使います.ubuntu Linux 16.04 (というかLinux kernel 4.4.0-104-generic) では実機に書いてあるインターフェースの番号とOSのデバイス名が以下の通り対応しています.

  • LAN1 : enp1s0
  • LAN2 : enp2s0
  • LAN3 : enp3s0
  • LAN4 : enp4s0

今回はLAN1 (enp1s0) をインターネット側,LAN2 (enp2s0) を内部ネットワーク側に設定します.ネットワークの構成は以下の通りにします.

  • ネットワーク: 10.0.0.0/24
  • DHCPで使うIPアドレスのレンジ: from 10.0.0.129 to 10.0.0.254
  • 内部ネットワーク側のIPアドレス: 10.0.0.1

この段階での設定は以下の通り.

/etc/network/interfaces
auto lo
iface lo inet loopback

auto enp2s0
iface enp2s0 inet static
    address 10.0.0.1
    netmask 255.255.255.0

PPPoEの設定

PPPoEのユーザ名とパスワードを握りしめて以下のコマンドを実行します.

$ sudo apt install
$ sudo pppoeconf

pppoeconfを実行するといい感じに設定を薦めてくれるので,それに従って質問に答えていると /etc/ppp/ 以下に用意してくれます. pppoeconf コマンドを実行する時点でPPPoE対向機器とLANケーブルで接続されている必要があるのでご注意ください.

NAT & F/W の設定

/etc/sysctl.conf に以下の行を追加.

/etc/sysctl.conf
net.ipv4.ip_forward=1

/etc/rc.local を編集.

/etc/rc.local
/sbin/iptables-restore < /etc/network/iptables
exit 0

本来は /etc/network/if-pre-up.d/ 以下にiptablesを読み込むスクリプトを置くのが筋みたいですが,それをやるとインターフェース起動より後に実行される docker サービスが起動時にiptablesの設定を上書きしてしまうようなので, /etc/rc.local で iptables-restore を実行します.

/etc/network/iptablesiptables-save の結果を保存して編集しました.元のdockerの設定もまぜつつ,サービス用のポートを空ける設定などを仕込んで,最終的に以下のようになっています.

/etc/network/iptables
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:DOCKER - [0:0]

# eth0 is WAN interface, eth1 is LAN interface, ppp0 is the PPPoE connection
-A POSTROUTING -o ppp0 -j MASQUERADE
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN

COMMIT

*filter
:INPUT DROP [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:DOCKER - [0:0]
:DOCKER-ISOLATION - [0:0]
-A FORWARD -j DOCKER-ISOLATION
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A DOCKER-ISOLATION -j RETURN

# Dockerへの通信は内 → 外へのパケットは通しても外 → 内への通信は通さないようにする
-A DOCKER -i ppp0 -p udp -m state --state ESTABLISHED -j ACCEPT
-A DOCKER -i ppp0 -p tcp -m state --state RELATED,ESTABLISHED -j ACCEPT
-A DOCKER -i ppp0 -j DROP

-A INPUT -i lo -j ACCEPT
-A INPUT -p tcp -m state --state ESTABLISHED,RELATED -j ACCEPT
-A INPUT -p udp -m state --state ESTABLISHED -j ACCEPT

# ホストのサービスとして開放するポートを絞る
-A INPUT -p tcp --dport 22 -s 10.0.0.0/24 -j ACCEPT
-A INPUT -p icmp -s 10.0.0.0/24 -j ACCEPT

# INPUTでも上記以外の通信は落とす

COMMIT

rsyslogの設定

後述しますが,ログは全てfluentd経由でAWSのS3に保存するようにするので,ホストOSのrsyslogもfluentdにログを飛ばすように設定します./etc/rsyslog.d/10-remote.conf を新たに作成して,以下の通り編集します.

/etc/rsyslog.d/10-remote.conf
*.* @127.0.0.1:5514

監視サービスの導入

監視サービスにはMackerelを使います.類似サービスとしてはDatadogNew Relicなどがあります.ちゃんと比較したわけではないですが,Mackrelは比較的シンプルに使いやすそうだなと思ったと,無料枠にカスタムメトリクスがあり,手軽に使えそうだということで選びました.

監視もできればDockerコンテナ上からやりかたったですが,無理やりコンテナからホスト上のデータを参照させるのもややこしいことになりそうだったので,素直にホストOSにエージェントをインストールしました.基本的な監視のエージェントは以下のようなスクリプトを1行実行することでインストールできます.

$ wget -q -O - https://mackerel.io/file/script/setup-all-apt-v2.sh | MACKEREL_APIKEY='2R8VBzXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' sh

詳しくは以下のページから参照できます.
https://mackerel.io/orgs/mizutani-home/instruction-agent

監視ルールも無料枠で10個設定できるようなのですが,ひとまずはデフォルトのconnectivityチェックと,CPU,Disk,Memoryの使いすぎ状態を検知するようなルールだけ動かしています.

Screen Shot 2018-01-07 at 22.01.41.png

サービスの設定

導入するサービス

以下のサービスをルータ内で動かします.

fluentd (ログ収集)

皆様ご存知,ログ収集ツールのfluentdです.今回のルータではfluentdにログを集中させ,一括管理します.主な受取元と配送先は以下のとおりです.

  • 受取元
    • 各dockerコンテナからlogging driverを通して送られてくる標準出力・標準エラーのログ
    • ホストOSのsyslog
    • softflowdから送られてくるNetflowのログ
    • dns-gazerから送られてくるDNSのクエリ・返信ログ
  • 送り先
    • ログは基本的にAWSのS3上に保存する
    • 一部のメトリックスはmackerelに送って監視する(今回の解説では未実装ですが,将来的に実装予定です)

ログに関してはnetflowログ,DNSログ合わせても1日3MBいかない程度で保存できます.東京リージョンだと料金は$0.025/GB(最初の50TB)で.1年で約1GBためるとしても,10年ためて月々 10GB * $0.025/月=月々30円弱で,元の目標に収まっている計算です.

unbound (DNSキャッシュサーバ)

昔からDNSサーバといえばbindが有名ですが,キャッシュサーバとして動かすだけならDNSのフルの機能が実装されているbindを使う理由はそれほどありません.unbound はキャッシュサーバとしての機能をメインにフォーカスして作成されたDNSサーバのようで,キャッシュ汚染耐性,パフォーマンス,設定の容易さなどが強みとのことです.設定も非常にシンプルでこれだけの設定内容でDNSキャッシュサーバとして動作します.

unbound.conf
server:
  port: 53
  interface: 0.0.0.0
  access-control: 10.0.0.0/24 allow

  logfile: ""
  verbosity: 2

kea (DHCPサーバ)

動的にIPアドレスを設定するDHCPも従来はInternet Systems Consortiumが実装していたいわゆるisc-dhcp-serverがよく使われていましたが,最近はISC自身が "Modern Open Source DHCPv4 & DHCPv6 Server" と銘打ってkeaというDHCPサーバを実装しています.Facebookもデータセンターでkeaを使っているというブログを公開しており,導入実績はいろいろとあるようです.

keaは従来のISC DHCPサーバと比べて以下の特徴があるようです.

  • RESTfulな設定変更などのAPIが提供されている
  • リースのDBにRDBS(PostgreSQL, MySQL)が利用可能になっている
  • DHCPv4とDHCPv6が分離している

今回は設定変更などの機能は使いませんが,リースDBとしてMySQLを利用するようにしています.

また余談ですが,ソースコードとしては非常に大きく,ソースコードからビルドしようとするとものすごい時間がかかります.(2013年版のMacBookProで2,3時間かかるという近年まれに見る長時間コンパイル)ご利用の際はバイナリ版を使うことをおすすめしておきます.

softflowd (Flow監視)

softflowdはネットワークインターフェースを監視して通信のフロー情報(送信元・宛先IPアドレス,IPプロトコル,送信元・宛先ポート番号,送受信データ量,パケット数,開始・終了時刻など)を取得します.

本当はntopngが提供している nprobe のほうがより詳細な情報を取得できたのですが,ちゃんとした運用として使うためにはライセンスの購入が必要とのことでした.お安くすませるという今回の設計には向かないと思い断念しました.

現状はひとまず softflowd で運用しようと考えていますが,今後別のツール or 自分で作ったツールで置き換える見込みです.

dns-gazer (DNS監視)

DNSでの問い合わせのログを取るためにdns-gazerというツールを使います(拙作のツールです).こちらもsoftflowd同様にネットワークインターフェースを監視して,DNSの問い合わせおよび応答のログを作成します.ログは直接fluentdに対してforward形式で出力し,今回はAWS S3にログを保存します.DNSの通信をキャプチャする類似ツールとして dnscap があるのですが,ログの出力方法や形式に難があったので別ツールとして自作しました.

softflowdもそうですが,DNSのログもセキュリティの目的で取得しています.と言ってもそこまで高度な分析をするということはなく,あとからブラックリストなどに掲載されているドメイン名やIPアドレスと通信をしていたことを確認することでマルウェアへの感染などを検出しようとしています.また(普通の自宅環境でそんなことをすることはほぼないですが)場合によってはインシデント発生時にフォレンジックができるようにする,という目的も兼ねています.

本当はこのログの利活用の部分も合わせて紹介したかったのですが,まだツール類などが未成熟のため,これについては後日改めて解説などを出したいと考えています.とりあえず今回はちゃんとログを残すというところだけで.

外部サービスの準備

Mackerel

MackerelについてはホストOSの設定の章で導入できているかと思います.IDファイル
/var/lib/mackerel-agent/id を利用するので,このファイルが存在していれば問題ありません.

AWS S3

先述したとおり,今回はログファイル保存用にAWS S3バケットを利用します.アカウント作成などについては省略します.

アカウントがある状態でコンソールが使えるようになったら,まずはバケットを作成します.今回は home-network というS3 bucketを用意したという前提で話を進めます.

バケットが作成できたら,次はAPIキーを取得します.本人のアカウントのAPIキーを使ってもできなくはないですが,権限が広すぎるのでS3にログを保存する専用のユーザを作成することをおすすめします.

ユーザを作成したら(あるいはする途中に)S3への書き込みができるようにするポリシーを作成して当該ユーザにアタッチします.設定するポリシー例は以下のとおりです.ログファイルを作成する s3:PutObject およびfluentdのout_s3プラグインがバケット上のファイルの存在有無などを確認できるよう s3:GetObject および s3:ListBucket の権限も付与します.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::home-network",
                "arn:aws:s3:::home-network/*"
            ]
        }
    ]
}

ポリシーの設定ができたら,IAMの「ユーザー」->「当該ユーザ名」->「認証情報」から「アクセスキーの作成」を選択し,いわゆる AWSAccessKeyIdAWSSecretKey の情報を取得し保存しておきます.

docker-composeでサービスが立ち上がるようにする

ここからは私が用意したdocke-compose構成を利用して環境構築することを前提に解説します.

$ sudo mkdir -p /opt
$ sudo chown $USER /opt
$ cd /opt
$ git clone https://github.com/m-mizutani/docker-based-home-router
$ cd docker-based-home-router

その後,

$ cp config.template.json config.json
$ vim config.json

として, config.json というファイルを生成し,必要な箇所をそれぞれ書き換えます.

{
  "s3": {
    "key": "AKXXXXXXXXXXXXXXXXXXXXXX",
    "secret": "Ib346xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "region": "ap-northeast-1",
    "bucket_name": "home-network"
  },
  "dhcp": {
    "interfaces": [
      "enp0s2"
    ],
    "pools": [
      "10.0.0.129 - 10.0.0.254"
    ]
  },
  "network": {
    "internal_gateway": "10.0.0.1",
    "subnet": "10.0.0.0/24"
  },
  "monitor": {
    "interface": "enp0s2"
  },
  "mackerel": {
    "api_key": "2R8xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  }
}

概ね見て分かるかなと勝手に期待してしまうのですが,ちょっとだけ解説を入れます.

  • dhcp/interfaces/ 以下にあるインターフェース名と monitor/interface は基本同じものになります.前者がkeaの設定,後者がsoftflowdとdns-gazerの監視インターフェースになります.
  • dhcp 以下の2つの項目はリスト型になっているので注意してください(一応リストに複数値を入れても動作するはずですが,今のところ未検証です)

config.json が作成できたら setup.py を実行します. Python3.6で動作を確認しています. 実行すると各種設定ファイルを自動生成します.

$ ./setup.py
2018-01-06 03:57:57,279 [DEBUG] config path: /opt/docker-based-home-router/config.json
2018-01-06 03:57:57,282 [INFO] creating fluentd config: /opt/docker-based-home-router/fluentd/fluent.conf
2018-01-06 03:57:57,286 [DEBUG] Found Mackerel API KEY in config
2018-01-06 03:57:57,286 [DEBUG] Found Mackerel ID file: /var/lib/mackerel-agent/id
2018-01-06 03:57:57,286 [INFO] creating kea config file: /opt/docker-based-home-router/kea/kea-config.json
2018-01-06 03:57:57,289 [INFO] creating env file for mysql: /opt/docker-based-home-router/mysql.env
2018-01-06 03:57:57,289 [INFO] creating env file for dns-gazer: /opt/docker-based-home-router/dns-gazer.env

ファイルの自動生成が終わったらイメージをビルドします.

$ sudo docker-compose build

これで立ち上げの準備が整いました.

docker-compose.yml

docker-composeの構成や各イメージの中身の詳細については基本的にコードを読んでくださいという感じなのですが,かいつまんでポイントを解説しておきます.

docker-compose.yml
  unbound:
    build: ./unbound
    restart: always    
    ports:
    - "53:53/udp"
    depends_on:
    - fluentd
    logging:
      driver: fluentd
      options:
        fluentd-address: localhost:24224
        tag: "docker.{{.ImageName}}.{{.ID}}"

unboundに限らずですが,logging driverはすべてfluentdコンテナに集中させてそれをS3に飛ばしています.したがってすべての depends_on にfluentdコンテナを指定しています.タグは docker.<image名>.<container ID> にしてみました.

docker-compose.yml
  kea:
    build: ./kea
    restart: always    
    network_mode: host

keaはクライアントのDHCP discover時に発生するブロードキャストパケットを受信する必要があります.dockerコンテナは基本的に内部ネットワークにいるので通常構成ではDHCP discoverパケットはコンテナまで届きません.いろいろいじればできなくはなさそうでしたが,今回はシンプルに --net=host に相当する設定を入れて対応しました.

docker-compose.yml
  kea-db:
    build: ./kea-db
    restart: always    
    ports:
    - 127.0.0.1:3306:3306
    env_file:
    - ./mysql.env
    volumes:
    - kea-db-vol:/var/lib/mysql

一方,keaのコンテナがnet=hostの状態だと links でmysqlコンテナと接続できなくなってしまうため,mysqlが3306/tcpをローカルに対してexposeし,keaが 127.0.0.1:3306 に接続するという構成になっています.

データベースの初期化スクリプトは kea-db というmysqlのイメージの方に仕込んであります.また,データベースのファイルはvolumeを設定して永続化しているので,再起動しても同じリースDBが引き継がれます.

docker-compose.yml
  fluentd:
    build: ./fluentd
    restart: always
    ports:
    - 127.0.0.1:24224:24224
    - 127.0.0.1:5514:5514/udp
    - 127.0.0.1:2055:2055/udp
    volumes:
    - fluentd-buffer:/var/log/fluentd

fluentdもバッファ用のディレクトリはvolume mountしてコンテナを再起動しても引き継ぐようになっています.ポートはそれぞれ以下の目的でexposeさせています.

  • 24224 : logging driver + dns-gazerのログ受信
  • 5514 : rsyslogの受信
  • 2055 : netflowの受信

docker-composeをマシン起動時に立ち上がるようにする

準備が整ったところで,最後に /etc/rc.local に起動用のスクリプトを追加します.最終的に以下のようになります.

/etc/rc.local
/sbin/iptables-restore < /etc/network/iptables
docker-compose -p router -f /opt/docker-based-home-router/docker-compose.yml --verbose up -d
exit 0

このあと,sudo /etc/rc.local などとすればコンポーネントがすべて立ち上がるはずです.この起動コマンドだとログなどがバックグラウンドに流れるため,デバッグが必要なときは -d オプションを消して docker-compose コマンドを実行するといいでしょう.

$ sudo docker-compose -p router -f /opt/docker-based-home-router/docker-compose.yml --verbose up

結果

できあがるものがルータなので目に見えてわかりやすい結果はでてきませんが,一応こういう感じに動いていますということで.

Screen Shot 2018-01-08 at 13.18.22.png

Mackrel上での監視画面です.2つトラフィック監視プロセスを動かしていますが,そもそもトラフィック量も全然少ない(昨晩の最大瞬間風速が1M Byte/ss = 8Mbpsほど)なので,CPUもまったく消費されてない状況です.メモリは昨晩再起動した時にcacheがガッと減っていますが,概ね1GB弱がusedになっています.

Screen Shot 2018-01-08 at 13.14.34.png

ログもこんな感じでS3に保存されていきます.それぞれ取り始めた時期がバラバラですが,保存されているサイズはだいたい以下のとおりです.

  • dns : 15MB (11日ほど)
  • docker : 20KB (2日ほど)
  • netflow : 3MB (3日ほど)
  • system : 130KB (11日ほど)

とりあえず,いきなりS3破産するということはなさそうです.

想定FAQ

ホストOSの設定にCehf,Ansible,Capistranoは使おうと思わなかったの?

頑張っても良かったんですが,

  • HW依存になる部分がちょいちょいある
  • サービスはそこそこ頻繁に追加・変更することを想定しているが,ネットワーク構成の変更は考えていない

という理由から今回はやめました.

今の時代IPoEじゃないんですか?

そういえば申請だけしてその後放置していたのを今思い出した.今度IPoE版をチャレンジしてみます.

OOのサービスは導入しないの?

今回の実装で概ね基本機能は網羅したと思っているのですが,面白サービスやおすすめのサービスがあればぜひお知らせください :pray:

雑な参考資料の一覧