Edited at

有線/無線LANルーターを作ろう!~後編. ソフトウェアの紹介と導入・iptablesによるファイヤーウォール設定

More than 1 year has passed since last update.

前回の続きです。今回は以下構成で解説を行っていきます。


  1. ソフトウェア説明・使い方

  2. ソフトウェアの導入: 必要機材

  3. ソフトウェアの導入: PC環境構築

  4. 最後に

  5. APPENDEX: ソフトウェア内部解説


1. ソフトウェア説明・使い方


ソフトウェア説明

作るのはこんな構成のルーターです。ユースケースは前編を参照ください。


  • 機能

このソフトウェアが実現する機能は、複数デバイスを束ねるLANを構築し、LANからインターネットに繋げるです。ごく普通のルーター。

利便性としては、LANに使用するデバイスは拡張可能で、pluginと設定さえ行えばLANに追加が可能という点がOSSチックな点だと思います。


  • モジュール概要

このソフトウェアがサポートする緑色の各モジュールに対する役割説明です。

モジュール
役割

Lan_router
ルーターのメイン処理。bridgeインターフェイスの作成、pluginを介して作成されたデバイスを追加し、LANネットワークを設定構築する。

plugin
各デバイスの制御を行うCライブラリ。ethernet, wifi(OS認識済み, nl802ドライバ)をサポート。新規追加も可能

Bridge LAN Interface
LANネットワークを束ねる仮想インターフェイス。LANのゲートウェイ、NAT転送、DHCPサーバー、DNSサーバーの役割と担う。

ethernet, wifi
今このソフトウェアで制御可能なデバイス。LAN向けに使用する。

WAN device
Network Manager等に制御された、インターネットに出るためのインターフェイス

上記のLAN側で使用するethernet, wifi, bridge LANネットワーク設定は全てjson形式の設定ファイルに記載。

confの書き換えで勝手にLan_routerがLANを構築します。実現方法は前回の必要技術を使います。

※ただしIPアドレスの振り方だけはioctlの方がやりやすかったので変えています。APPENDIXで紹介。


使い方

ビルド

1.下準備として、コンパイラautotools(autoconf, gcc), libtool, cmake, gcc-c++, janssonをインストールする。

2.design_pattern_for_cをcloneしてビルド、インストールする。(このルーター向けにいくつか更新したため、release版だとこのルーターでは使えません)

#libev4, lievent-devがない環境

./configure --disable-threadpool-libevent --disable-threadpool-libev && make && make install)

3.ここからソースコードをダウンロードする。

4.フォルダに移動して以下を実行

cmake .

make

使用方法


  1. confを環境に合わせて書き換え。

  2. 起動


    1. lan_router/srcに移動

    2. rootで./lan_router ../conf/setting.jsonを実行。



※default gwに設定されているデバイスをWAN側インターフェイスとして扱い、NATの転送設定を行います。実行前にdhclientやNetworkManagerを利用して外部ネットワーク接続が可能な状態にしてください。

confの使い方は以下となります。


setting.conf

{

"bridgeif": {
"name": "br0",
"ipv4": "192.168.1.1",
"netmask": "255.255.255.0",
"dhcpv4start": "192.168.1.100",
"dhcpv4end": "192.168.1.254",
"lease-time": "86400"
},
"../lib/libeth_landevice.so": {
"type": "ethernet",
"name": "eth0"
},
"../lib/libwifi_landevice.so": {
"type": "wifi",
"name": "wlan0",
"freq": "2437",
"ssid": "testssid",
"pass": "testpassword"
}
}

最初の"bridgeif":{}がbridge(LANネットワーク)の設定。上からbridge IF名、IFに設定するIPとnetmask, dhcpで割り当てる範囲の開始と終了, リース時間(秒)となっています。

次の"../lib/XXX.so"がプラグインのパスになります。これは相対パスで指定。


  • typeが"ethernet"の場合:


    • libeth_landevice.soを使用。ifの立ち上げとipの初期化を行います。



  • typeが"wifi"の場合:


    • libeth_lanwifi.soを使用。無線LANアクセスポイントを立ち上げます。nameキーから順にIF名、周波数、ssid、WPA2-PSKのパスワードになっています(暗号方式は固定)。



  • 新しいプラグインを作成


    • 上記以外にプラグインを作成してconfの内容を定義することも可能です。プラグイン内でconfの情報が読めるようになるので、比較的簡単に追加出来ると思います。




2. ソフトウェアの導入: 必要機材


  • 余っているLinux PC

  • WAN側インターフェイス用のデバイス

  • LAN側インターフェイス用のデバイス

があれば動作します。私はethernet2つ(WAN, LAN用)、Linux対応wifiカードを使用しています。

PC類は3年前に買ったものを流用。

PC:Intel NUC D34010WYK + ケーブル類

OS: CentOS 7.5.1804 (Core)

Wifi:Intel Dual Band Wireless-AC 7260 867 Mbps+ Bluetooth 4.0

アンテナ:airgain N2420 4dBi WIFI

ethernetの口は1つなので2本目をUSB ethernetで準備。ヨドバシカメラで1680円くらいでした。

ethernet(USB):tp-link UE300


3. ソフトウェアの導入: PC環境構築


ソフトウェア導入手順


  1. 以下がなければインストールします。

アプリ
パッケージ
備考

brctl
bridge-utils

ifconfig
net-tools
ファイヤーウォール設定スクリプトで使用

ip
iproute

iptables
iptables

dhcpd
dhcp
centOSの場合。Ubuntuだとisc-dhcp-serverです。時間があればdnsmasq版も作成

dnsmasq
dnsmasq
DNS割り振りの為に使用。起動していればよい

wpa_supplicant
wpa_supplicant

gunzip
unzip
ファイヤーウォール設定スクリプトで使用

2.ビルドしたパッケージをビルド、インストールします。デフォルトでは/usr/local/bin, /usr/local/lib, /usr/local/conf配下に各ファイルが格納されます。

cmake .

make
make install

※無線LAN APの接続は確認できたのですが、バグ or 今の環境の問題ですぐ別の無線LAN APに切り替わってしまうため、長期運用は未確認です。

ほぼうちのネットワーク環境が原因だと思っていますが、もし使ってみようという奇特な方で何か問題あったらコメントいただけると幸いです。


ファイヤーウォールの設定

ここが一番ハマったところ && 今の課題です。

CentOS 7の環境だとデフォルトでfirewalldが動作しているのですが、前回の方法ではLAN側ネットワークのポートフォワディングがうまくできず。

firewallの口をある程度開けても外部へのping 8.8.8.8(GoogleのDNSサーバー)は通るのにHTTPアクセスははじかれる状態でした。firewalldが各ポートのフォワーディングをしてくれない状態

恐らくLAN側ネットワーク向けに新たにゾーンを作ってあげないといけないのかな?というところで足踏み。以下の2択で悩みました。


  1. firewalldで適切な設定を行うようなソフトウェアにする。

  2. firewalldを無効化し、iptablesで強固なfirewallを設定した上でポートフォワディングする。

結果私は案2を選択。理由は3つ。


  1. CentOS以外はfirewalldがデフォルトインストールいるわけではない。

  2. firewalldを使ったとしても強固な対策は必要になる。

  3. 強固なiptables設定は日本語の情報が多い

というわけで、このソフトウェアではfirewalldを無効化 && スクリプトでのiptablesによるfirewall設定を行うことにします。

systemctl disable firewalld

systemctl stop firewalld

スクリプトはこちら、linux_router/scriptに置いてあります。以下スクリプトの解説。


iptablesによるファイヤーウォールの設定の設定

設定は時間がかかる処理があるので2段階に分けて行います。


  1. ブラックリストの作成

  2. iptablesによる各種セキュリティ対策

設定はこれらを参考にしています。

iptablesの設定ファイルをシェルスクリプトを利用して動的に作成

iptablesの設定でサーバー攻撃対策と海外からのアクセスを制限


ブラックリストの作成

ルーターとして公開するからには、海外から踏み台サーバーにされないようにしたい。というわけで海外IPからのアクセスを遮断します。

コマンド
機能
備考

./countryip.sh
国のipアドレス一覧を更新
定期的に更新するのがベター

./countryip.sh -r
国のipアドレス一覧を元にDDosの多い国(2017年Q3ベース)のIP一覧を表示。
ここでは中国, 韓国, ロシア, イギリス, オランダ, 香港を対象にしてます
環境に合わせて対象国を変えてください。

./firewall_BLACKLIST.sh
BLACKLISTチェーンを作成
./countryip -rがベース。定期的に更新するのがベター。時間がかかります。

./countryip.sh -a
国のipアドレス一覧を元に受け入れる国のIP一覧を表示。
日本、アメリカを対象にしています。
環境に合わせて対象国を変えてください。

./firewall_WHITELIST.sh
WHITELISTチェーンを作成
./countryip -aがベース。定期的に更新するのがベター。時間がかかります。

./countryip.shはこちらのサイトで更新してくれている国別IPアドレスを使用しています。

使い方はこんな感じ。/root/script/において使用する想定です。


  1. /root/script/に各スクリプトを格納

  2. ./countryip.shを実行し、IPリスト(http://nami.jp/ipv4bycc/cidr.txt.gz)をダウンロード


  3. ./firewall_BLACKLIST.sh国名コードからBLACKLIST対象のアドレスを抜き出し、iptablesのチェーンを作成

逆にWHITELISTを利用した制限をする場合は、./firewall_WHITELIST.shでWHITELISTチェーンを作成します。(動作未確認)

時間のかかるけど更新が入るものなので、cronで毎日or毎週更新するようにするといいと思います。

これを次に紹介するファイヤーウォール設定に利用します。


ファイヤーウォールの設定

参考ページの受け売り。現状のものを紹介します。

使い方: ./firewall.sh WANインターフェイス名 ブリッジインターフェイス名 (例: ./firewall.sh eno1 br0)

最初に設定をリセットしてINPUT, FORWARDは全遮断します。


firewall.sh

#reset firewall

iptables -F INPUT
iptables -F OUTPUT
iptables -F FORWARD

#set base
iptables -P INPUT DROP
iptables -P OUTPUT ACCEPT
iptables -P FORWARD DROP


まず既存のコネクションはキープしておきます。SSHログインが切れたらしんどい。


firewall.sh

## Connection

iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT

DROP系設定は以下の対策をします。詳細は理解しきれていない長くなるので省略


firewall.sh

#IP Spoofing

iptables -A INPUT -i $WANIF -s 127.0.0.1/8 -j DROP
IPAREA=`ip addr show ${LANIF} | grep "inet " | awk -F" " '{print $2}'`
LOCAL_IP=`echo $IPAREA | awk -F"/" '{print $1}' | awk -F"." '{print $1 "." $2 "."$3 ".0"}'`
LOCAL_AREA=`echo $IPAREA | awk -F"/" '{print $2}'`
iptables -A INPUT -i $WANIF -s ${LOCAL_IP}/${LOCAL_AREA} -j DROP

##############################
#ping attack (Large
PING_MAX=85
iptables -N PING_ATTACK > /dev/nul 2>&1
iptables -A PING_ATTACK -m length --length :${PING_MAX} -j ACCEPT
#if you want to save log
#iptables -A PING_ATTACK -j LOG --log-prefix "[IPTABLES PINGATTACK] : " --log-level=debug
iptables -A PING_ATTACK -j DROP
#ping attack (length
iptables -A PING_ATTACK -p icmp --icmp-type echo-request -m length --length :${PING_MAX} -m limit --limit 1/s --limit-burst 4 -j ACCEPT
#Add PING_ATTACK
iptables -A INPUT -p icmp --icmp-type echo-request -j PING_ATTACK

#Smurf attack
iptables -A INPUT -i ${WANIF} -d 255.255.255.255 -j DROP
iptables -A INPUT -i ${WANIF} -d 224.0.0.1 -j DROP
iptables -A INPUT -i ${WANIF} -d ${BROADCAST_WAN} -j DROP
iptables -A INPUT -i ${WANIF} -d ${BROADCAST_LAN} -j DROP

#Smurf forward
sysctl -w net.ipv4.icmp_echo_ignore_broadcasts=1 > /dev/null

#SYN cookies
sysctl -w net.ipv4.tcp_syncookies=1 > /dev/null

#rp_filte
sysctl -w net.ipv4.conf.${WANIF}.rp_filter=1 > /dev/null

#ICMP redirect
sysctl -w net.ipv4.conf.${WANIF}.accept_redirects=0 > /dev/null

#Soruce route
sysctl -w net.ipv4.conf.${WANIF}.accept_source_route=0 > /dev/null

#Disable tcp timestamp
sysctl -w net.ipv4.tcp_timestamps=1 > /dev/null


ブラックリストチェーンを追加


firewall.sh

#Create Blacklist

#${COUNTRY_BLACKLIST}#./firewall_BLACKLIST.sh実行は別途
iptables -A INPUT -j BLACKLIST

必要なポートだけ受け入れ。自分はとりあえずDNS, HTTP/HTTPS, メール系, ローカルのみSSH(sshd_configから抜き出し)があればいいかと


firewall.sh

##SSHはローカルだけ

LOGIN=`cat /etc/ssh/sshd_config | grep '^#\?Port ' | tail -n 1 | sed -e 's/^[^0-9]*\([0-9]\+\).*$/\1/'`
iptables -A INPUT -p tcp -i ${LANIF} --dport $LOGIN -j ACCEPT

accept_port_tcp() {
for port in $1
do
#if you want to accept only whitelist, please change ACCEPT to WHITELIST
iptables -A INPUT -p tcp --dport $port -j ACCEPT
iptables -A FORWARD -p tcp --dport $port -j ACCEPT
done
}

accept_port_udp() {
for port in $1
do
iptables -A INPUT -p udp --dport $port -j ACCEPT
iptables -A FORWARD -p udp --dport $port -j ACCEPT
done
}

PORT_UDP=
PORT_TCP=
#DHCP
PORT_UDP+="67 68 "

#DNS
PORT_UDP+="53 "
PORT_TCP+="53 "

##http setting
#http
PORT_TCP+="80 "
#https
PORT_TCP+="443 "

##main setting
#smtp
PORT_TCP+="25 "
#smtps
PORT_TCP+="465 "
#pop3
PORT_TCP+="110 "
#pop3s
PORT_TCP+="995 "

accept_port_tcp "$PORT_TCP"
accept_port_udp "$PORT_UDP"


こちらのiptables設定よりHTTPのDDos対策など、もっと強固にすべき設定が色々あるなと思いつつ、海外IP排除である程度の対策は出来たかなと思い一旦公開させていただきます。そうしないとモチベが~

本格導入にあたり、ちょいちょい更新すると思います。セキュリティ対策は強い知識と意志が必要ですね。精進します。


最後に

ルーター構築、徐々に外部アクセスが出来るようになる過程が非常に楽しかったです。

ただ最後のファイヤーウォールは「僕はちょっとした気持ちでルーターを作ろうと思っていたのに、なぜがっつりファイヤーウォールについて考えているんだろう」とは思っちゃいました。でもルーターを作る = 誰かに侵入される/踏み台にされるリスクが存在するので、このソフトウェアを作成する上で必須情報だからしょうがない。

こういう必要技術の連鎖ってものづくりをすると必ずおきますね。

まだ課題は色々あるのでちょこちょこ細かな更新をしていくと思います。

今後の課題:


  • ファイヤーウォール、これでいいのか吟味が必要

  • ファイヤーウォール設定とlan_routerの連動をしたい

  • ブラックリストの自動更新を行う仕組みを導入したい

  • lan_routerをインストールできる仕組みが欲しい

  • インストールするパッケージが多くてめんどい


APPENDEX: ソフトウェア内部解説

ファイヤーウォールの話に記事の本質を奪われたので、内部の話はAPPENDEXで。


クラス構成

こんな感じ。使用するプラグイン・その設定はjson形式の設定ファイルに全部記載して使います。

基本コマンドを叩くだけなので、役割が発散しないようクラス分けは細かめにしました。

また、裏目的だった「自作OSSのbuilderを利用」してデバイス初期化を隠ぺいしています。

プラグインへのインプットはjson形式の設定ファイルパーサーポインタを丸ごと渡しているので、プラグイン追加も楽な構成になっていると思います。

LANIPManagerGWConfigurator(bridge作成, NAT設定担当)とDHCPConfigurator(DHCP/DNSサーバー起動担当)を利用してLAN制御。

LANManagerJsonParser、Builderで生成したLANInterfaceの情報、LANIPManagerをまとめて保持し、それらを利用してmainに使ってもらうメソッドを提供しています。

プラグイン側はlan_if_loadlan_if_upというAPIを実装してもらえば、builderが勝手にそのAPIを呼んでくれます。

lan_if_loadはデバイス認識用、lan_if_upは立ち上げ用です。

ここではethernetとwifiを提供。

といっても私の環境ではどちらもデフォルトでデバイス認識しているので、実質lan_if_upしか書いてません。


使用言語

メインはC++, プラグインはCです。

基本コマンド叩くだけなのでPythonやRubyの方が書きやすそうだったのですが、builderを利用する為C++に。目的と手段が逆転してます

DHCPConfiguratorに一つインターフェイスを設けてDHCPサーバー差し替えが可能なようにしたかったのですが、LANIPManagerに手を加えず手軽に差し替えられる構成がうまく作れなかったので止めました。

dnsmasq版も作るのでもう一度トライはしてみますが、DHCPサーバーを別のものにしたい場合はDHCPConfiguratorを丸ごと差し替えてください。


Jsonパーサー

設定ファイルはJson。パーサーにはjanssonを使います。

こちらの記事で見つけたのですが、めっちゃ分かりやすいです。

使った機能だけ抜粋して紹介。

機能
API使用例
備考

JSON形式文字列のロード
json_t * object= json_loads(文字列, 0, &error);
errorはjson_error_t error;。objectからAPIで値を取得します。

JSON objectが{"key":"val"}の場合, keyと対応するvalueの取得
json_t * json_value = json_object_get(object, key);

ロードしたJSONが{"key":"val"}の場合, forで回す
json_object_foreach((json_t *)lan_info, key, value) { //処理 }

ロードしたJSONが文字列の場合
const char * value = json_string_value(json_value);

基本この辺りで事足りると思います。CのライブラリなのでCでも使えるし、各種APIの名前が直感的でわかりやすいです。

今回の設定ファイルは以下の構成。

{

"key":{
"key1":"val1",
"key2":"val2"
},
...
}

こんな感じの制御をしました。


ロード部分

        //設定ファイルロード

json_error_t error;
_setting_master = json_loads(input_string.c_str(), 0, &error);
if ( _setting_master == NULL ) {
fputs(error.text, stderr);
exit(-1);
}

//key:{}をmap化。{}部分のjson_t *を各自が利用する。
const char *key;
json_t *value;
json_object_foreach(_setting_master, key, value) {
std::cout << key << std::endl;
if( key == _lan_name) {
lan_info = value;
} else {
plugin_map[key] = value;
}
}



値取得API

//{}のobjectとkeyを渡して値を取得

const char * JsonParser::get_string(const json_t * object, const char * key) {
json_t * json_value = json_object_get(object, key);
if(json_value == NULL) goto errend;
{
const char * value = json_string_value(json_value);
if(value == NULL) goto errend;

return value;
}
errend:
fprintf(stderr, "setting error, please check key %s\n", key);
exit(-1);
}



インターフェイス制御

ioctlが想像以上に簡単でした。socketをopenした後は3行で済みます。

雑いソフトなのでエラーチェックがないけど多めに見てください。

//AF_INETでソケット作成

int fd = socket(AF_INET, SOCK_DGRAM, 0)

//IF名をコピー
struct ifreq ifr;
strncpy(ifr.ifr_name, _bridgeif, IFNAMSIZ-1);

//例えばifをupするならこの3行
ioctl(fd, SIOCGIFFLAGS, &ifr);
ifr.ifr_flags |= (IFF_UP | IFF_RUNNING | IFF_DYNAMIC);//IFF_DYNAMICはdown時にIPを削除するオプション
ioctl(fd, SIOCSIFFLAGS, &ifr);

//IPアドレスの文字列からIPを振るならこんな感じ
struct sockaddr_in * s_in = (struct sockaddr_in *)&ifr->ifr_addr;
inet_pton(AF_INET, ip, &s_in->sin_addr.s_addr);
ioctl(fd, SIOCSIFADDR, ifr);

close(fd);

オプションはNETDEVICEのmanページを参照ください。


参考


  • CentOS firewall設定

firewall設定

CentOS 7 firewalld よく使うコマンド

ハマる前に理解する「Firewalld」の設定方法、「iptables」との違い

iptables設定

俺史上最強のiptablesをさらす

iptablesの設定ファイルをシェルスクリプトを利用して動的に作成

iptablesの設定でサーバー攻撃対策と海外からのアクセスを制限

上記の深堀に使用

NTにおけるIPソースアドレス詐称

13.1. 戻り経路フィルタ (Reverse Path Filtering)

― 世界の国別 IPv4 アドレス割り当てリスト ―

DDoS攻撃の攻撃元、 6割超が中国- 2017年Q3

国名コード


  • C++: 沢山あるけど役に立ちそうなところだけ抜粋。

例外の書き方: もう少し例外を使用しても良いのではないか...

staticの実装部はstaticつけちゃダメ: static メンバ関数を定義したら cannot declare member function 'static xxx' to have static linkage

extern Cの付いていないCライブラリへの対処

C++のマングルとextern "C" {

extern Cの楽な書き方

libev で見つけた, ちょっとした extern "C" マクロスニペット


  • CMake:

基礎: ごく簡単なcmakeの使い方

ライブラリ追加に利用: 初めてのcmake備忘録

自作ライブラリ: CMakeを使って自作ライブラリをビルド&インストールしてみたまとめ


  • Json

C言語から使えるJSONパーサ、jansson がとても直感的で良い

他のJsonライブラリも沢山紹介されています。


  • インターフェイス制御

単純なIPアドレス取得プログラム

他IP設定、ifのupも参考にしています。