はじめに
みなさん、こんにちは。
現在、完全プライベートで開発しているコンテナホスティングプラットフォーム(以下、PF)の開発において、Firewallについて深く知ることがなかったので、今回の記事ではFirewallについて自分なりにまとめた記事を執筆したいと思います。因みに、筆者はネットワーク嫌いなので(元々、ネットワークのエンジニアだったけども、運用やればやる程、気分が悪くなるレベルです。ただし、誤解を恐れずに言うと極限まで嫌っているので今は、勉強する気概があります。また、今でいうDevOpsのような自分の責務の範囲でできる運用なら好きです。)、殴り書きなのはご了承下さい汗
事の発端
詳細は上のPRから↑
現在、PF開発においてバックグラウンドにjail(FreeBSD OSの準仮想化機構)を操作できるライブラリであるlibioc
のお手伝いをちょくちょくしています(と言っても、Rejectばっかりですが汗)。
このlibiocをコンテナホスティングPFで動くバックグラウンドライブラリとして利用しています。PFの開発では、ブラウザ上でコンテナを操作できる機能 + ローカルルーティング(もしくはNAT)の2つを重点において開発を進めています。
そこで、バックグラウンドライブラリ側でFirewallの設定を出力できるようなWrapperを入れてくれないかという事でPRを出していました。
結果としては、コンテナマネジメントライブラリとしてFirewallの機能は本質じゃないとの事でRejectされました。
その過程の途中で、気になった事がありました。
それは、現在libioc
でipfwのルールをadd, deleteできるWrapperが入っているのですが、他のFirewallではなく(PF, NPF等)なぜ、ipfwにしたのかという質問にこのような話が出てきました。
要約するとこんな感じ
ARP Spoofingによるなりすましを軽減するために、ブリッジ上でLayer 2レベルでのフィルタリングが必要となったため、ipfwを選定した。
(ホストブリッジ <> epair <> ブリッジ(ここでフィルタリング) <> epair <> jail).
ここで疑問だったのが、この答えは他のFirewallマネジメントツールと比較して、ipfwによるフィルタリングをする事の理由となっているのかどうか
という事でした。
その事を尋ねると、今度はこのような答えが返ってきました。
要約するとこんな感じ
ipfwはOSI参照モデルのLayer 2上のマネジメントツールであり、他のツールはLayer 3レベルという事を意味する。
jailが同じLayer 2の空間で動くドメインであるため、このフィルタリングを導入しているに過ぎない。
従って、ルーティング(もしくはNAT)のようなLayer 3レベルの設定をするのであれば、決してipfwを使う理由にもならないし、他のツールと比較してipfwが優れているという話でもない。
衝撃を受けた答えでした。今まで、そんな事気にせずFirewallを使っていたので、段々と各Firewallマネジメントツールについて調べる興味が湧いてきました。
という事でFirewallについて調べていきたいと思います。
Layer 2のRFCについて
Firewallの事を調べる前にまずは、今回の問題となったARP Spoofingについて調べます。その前に、もう一つレイヤーを高くしてLayer 2とはそもそも何かまとめます。
まず、国際標準化機構(ISO)は、異なるベンダーが実装した製品が相互通信できるように通信に必要な機能を7つの階層に分け、機能を分割し、複雑になりがちなネットワークプロトコルを単純化するためのモデルとして(プロトコル)Open System Interconnection Reference Model(OSI参照モデル)が提唱しました(なお、OSI参照モデルは実際のネットワークで利用されることはあまり(?)なく、IETFが提唱しているTCP/IPプロトコルスタック{スタックなのでこれも階層構造}の仕様からネットワークで利用されるプロトコルが実装される事が主流であり、IETFもOSI参照モデルと関係性を持たせようとしていない、、、ここら辺非常にややこしいです)。
一先ず、話を簡単にするためにこの記事では一旦OSI参照モデルだけを考慮して記事を執筆していきます。
このOSI参照モデルの7層のうち、1層の物理層、2層のデータリンク層はIEEE
のワーキンググループである802が1層と2層をまたぐLocal Area Network(LAN)の標準化を進めています。
更に、IEEEではデータリンク層をMAC副層とLLC副層という二つの副層として分けています(RFC読む限り、ここも厳密には違うかも、、、IEEEが策定している物理層部分は、OSI参照モデルにあたる物理層 + データリンク層の内MAC副層が該当
し、IEEE802.2でLLCの標準化を進め、これがOSI参照モデルのデータリンク層にあたる考えっぽい
、、、OSIのデータリンク層が更にMAC副層とLLC副層に分割されているポンチ絵よく見るんですけどね)。
ちょっとまとめるためにポンチ絵書きます。
ちょっとここら辺自信ないので、誰か教えて下さい。
ARPについて
続いて、ARPについてまとめます。ただし、ここでは元となったRFCが何か物凄く読みにくかったので(ほぼ私情だけど)、それは読まずにタネンバウム先生のコンピュータネットワークからのまとめです。
基本データリンク層(L2)のネットワークインタフェースカードはネットワーク層(L3)のIPアドレスについては、何も知りません。イーサネットの場合、固有の48bitのMACアドレスが付いており、宛先MACアドレスを知っていればイーサネットフレームを送れる訳です。ただし、L2を超えるネットワークでは相手を識別するためのユニークなアドレスが必要となります。ということで、ホストに割り当てられているIPアドレスとNICに付与されているMACアドレスのマッピング(ペアって言った方が正しい?)してあげる必要があります。
そこで用いられるプロトコルがAddress Resolution Protocol(ARP)
です。
ARPで用いられるデータの種類はARPリクエスト
とARPリプライ
の2つです。
- 宛先IPアドレスに対応するMACアドレスを知るためにARPリクエストで同一LAN内でブロードキャストを行う
- ARPリクエストを受け取ったマシンがそれぞれIPアドレスをチェックする。対応するIPアドレスを持つホストがARPリプライ(ユニキャスト)で返信を行う(L3を超える場合は、デフォルトゲートウェイとなるルータのMACアドレスを知るために、ルータがリプライする)。
非常に容易な構造でマッピングが行えます。
ARP Spoofingについて
その上でARP Spoofingとは宛先IPアドレスに対応するMACアドレスを持つのは自分だと、偽装の応答を行うことにより、間違ったマッピングをさせることです。
この場合、正しいARPリクエストがブロードキャストで偽ホストに到達し、正しい宛先ホストよりも早く返せば偽装できるという1つのケースだけだと思いますが、これ多分、定期的に偽のARPリプライをユニキャストで送るだけでも間違ったマッピングをさせることができそうですね、、、(ただし、こちらはOSレベルで受け取らないようにできそうな気もしますが)
- ケース1
- ケース2
今回のlibiocでどういう方法でARP Spoofing対策しているかはちょっとコードを追っかけられていませんが、ARPについて、ARP Spoofingの攻撃の容易さと言うのは理解できました。確かにこれはL2レベルでコンテナ作っているのであれば、気にする所ですね。
他にもSpoofingできそうなシナリオ考えられそうですが、ここまでにしておいて実際Firewallについて調べていきます。
Firewallの設定の要件について
ここから、Firewallを調べていきたい訳なのですが、そもそも、Firewallがどこまでフィルタリングするべきか(求められているか)分からないと調べようがないんじゃないか?
と思ったので、そんなRFCあるのかなと思ったら実際ありました。
https://www.ietf.org/rfc/rfc2979.txt
正直、目から鱗でした。Firewallの設定って運用のノウハウから作る(俺が考える最強のFirewallの設定)ものだと考えていたので、こういう設定を標準化するようなRFCがあった事自体驚きです。
ただし、中身はFirewallの振る舞いや特徴(パケットリレーやフィルタリングなど)、そもそもの要件から、Firewallはどうあるべきか問うものなので、具体的なこういうシナリオに関してはこうするべきという設定例が載った話ではない模様です(インターネットからきたTELNETやRLOGINのパケットはdropすべきとかそういう本当に最低限のレベルでの設定内容は書いているけども)。
以下、3. Firewall Requirementsからの抜粋です。
The introduction of a firewall and any associated tunneling or
access negotiation facilities MUST NOT cause unintended failures
of legitimate and standards-compliant usage that would work were
the firewall not present.
要約するとこんな感じ?
firewall自体、およびトンネリングもしくはネゴシエーション機構の導入として、Firewallが存在しなかったらうまくいくであろう、
正当かつ準拠した標準的な使い方に対して、意図しない(意図しないと言うのは誰から?送信元ホストのこと?)失敗を引き起こしてはならない。
ただし、RFCの具体例を見るとこれパケットフィルタ型のFirewallだけのRFCっぽいですね、ちょっと後から気づくことがあり、まとめるのが非常に難しい、、、一旦ここではパケットフィルタ型のFirewallだけを考えます。
と言う事で、基本的にはFirewall(パケットフィルタ型)のあり方としては意図しない失敗をさせない
と言うのが一番大事かなと思います。
ipfwについて
と言う事で、何回か遠回りしましたが、ようやく具体的なFirewallについて調べていきます。
まず、話題となったipfwです。ipfwはFreeBSD用に開発されたFirewallツールです。
githubにもリポジトリが載っているのでここのコードを追っかけていきます。なお、将来に向けた修正のためか、一部の機能は/sys/netpfilディレクトリに入っているコードも参照するようになっています。
今回は、/sbin/ipfwの方を見ます。
まずは、main.cのhelpコマンド部分
(中略)
| ADDR: [ MAC dst src ether_type ] \n |
|:--|
| [ ip from IPADDR [ PORT ] to IPADDR [ PORTLIST ] ]\n |
| [ ipv6|ip6 from IP6ADDR [ PORT ] to IP6ADDR [ PORTLIST ] ]\n |
MACアドレスによるルール決めもできる模様ですね。
続いて、本体
https://github.com/freebsd/freebsd/blob/master/sbin/ipfw/ipfw2.c
(中略)
/*
* Fetch and add the MAC address and type, with masks. This generates one or
* two microinstructions, and returns the pointer to the last one.
*/
static ipfw_insn *
add_mac(ipfw_insn *cmd, char *av[], int cblen)
{
ipfw_insn_mac *mac;
if ( ( av[0] == NULL ) || ( av[1] == NULL ) )
errx(EX_DATAERR, "MAC dst src");
cmd->opcode = O_MACADDR2;
cmd->len = (cmd->len & (F_NOT | F_OR)) | F_INSN_SIZE(ipfw_insn_mac);
CHECK_CMDLEN;
mac = (ipfw_insn_mac *)cmd;
get_mac_addr_mask(av[0], mac->addr, mac->mask); /* dst */
get_mac_addr_mask(av[1], &(mac->addr[ETHER_ADDR_LEN]),
&(mac->mask[ETHER_ADDR_LEN])); /* src */
return cmd;
}
まずは、ipfw_insn
です。中身は構造体であり、各エントリは対応するアドレス長を格納できるように、このipfw_insnから派生・拡張したものを定義しています。
typedef struct _ipfw_insn { /* template for instructions */
u_int8_t opcode;
u_int8_t len; /* number of 32-bit words */
#define F_NOT 0x80
#define F_OR 0x40
#define F_LEN_MASK 0x3f
#define F_LEN(cmd) ((cmd)->len & F_LEN_MASK)
u_int16_t arg1;
} ipfw_insn;
* This is used for MAC addr-mask pairs.
*/
typedef struct _ipfw_insn_mac {
ipfw_insn o;
u_char addr[12]; /* dst[6] + src[6] */
u_char mask[12]; /* dst[6] + src[6] */
} ipfw_insn_mac;
MACアドレスの場合、ipfw_insn_macを使います。ipfw_insn_macは送信先MACアドレス + 宛先MACアドレスを格納する配列と、送信先 + 宛先のマスク(MACアドレスを16bit毎に区切り、MACアドレスのどこまでが重要なのか表現する識別子)のペアが入ります。
よくよく考えたら、ipfwのルールを設定する際はコマンドの入力の長さや正当なものかチェックしなければならない機構が必要なので、もう一個上の関数部分も調べなきゃいけないのですが、一旦省略して次へ。
static void
get_mac_addr_mask(const char *p, uint8_t *addr, uint8_t *mask)
{
int i;
size_t l;
char *ap, *ptr, *optr;
struct ether_addr *mac;
const char *macset = "0123456789abcdefABCDEF:";
if (strcmp(p, "any") == 0) {
for (i = 0; i < ETHER_ADDR_LEN; i++)
addr[i] = mask[i] = 0;
return;
}
optr = ptr = strdup(p);
if ((ap = strsep(&ptr, "&/")) != NULL && *ap != 0) {
l = strlen(ap);
if (strspn(ap, macset) != l || (mac = ether_aton(ap)) == NULL)
errx(EX_DATAERR, "Incorrect MAC address");
bcopy(mac, addr, ETHER_ADDR_LEN);
} else
errx(EX_DATAERR, "Incorrect MAC address");
if (ptr != NULL) { /* we have mask? */
if (p[ptr - optr - 1] == '/') { /* mask len */
long ml = strtol(ptr, &ap, 10);
if (*ap != 0 || ml > ETHER_ADDR_LEN * 8 || ml < 0)
errx(EX_DATAERR, "Incorrect mask length");
for (i = 0; ml > 0 && i < ETHER_ADDR_LEN; ml -= 8, i++)
mask[i] = (ml >= 8) ? 0xff: (~0) << (8 - ml);
} else { /* mask */
l = strlen(ptr);
if (strspn(ptr, macset) != l ||
(mac = ether_aton(ptr)) == NULL)
errx(EX_DATAERR, "Incorrect mask");
bcopy(mac, mask, ETHER_ADDR_LEN);
}
} else { /* default mask: ff:ff:ff:ff:ff:ff */
for (i = 0; i < ETHER_ADDR_LEN; i++)
mask[i] = 0xff;
}
for (i = 0; i < ETHER_ADDR_LEN; i++)
addr[i] &= mask[i];
free(optr);
}
get_mac_addr_maskでMACアドレスとマスクを返します。送信元、宛先のMACアドレスがany
となっているものは、MACアドレスとマスクを0埋めしてあげています。
次のstrsepなのですが、strsepで、'&/'を区切り文字とした最初の文字列を返します。これは、ipfwで指定できるMACアドレスの送信元と宛先の区切りとして'&/'を指定しているからです。
次のif (strspn(ap, macset) != l || (mac = ether_aton(ap)) == NULL)
で、MACアドレスかどうか確認しているのですが、これ、ether_atonでチェックするだけじゃダメなんですかね?
続いてlong ml = strtol(ptr, &ap, 10);
の部分でアドレスを除いた部分、つまりはアドレスマスクの長さですね、それを調べるためにstrtolで10進数変換しています。
次の
if (p[ptr - optr - 1] == '/') { /* mask len */
for (i = 0; ml > 0 && i < ETHER_ADDR_LEN; ml -= 8, i++)
mask[i] = (ml >= 8) ? 0xff: (~0) << (8 - ml);
ですが、/で指定する場合はbit単位でどこまでのbitが重要なのかを処理します。
ここややこしいので、まずは&単位で区切った時の方を見ます。
} else { /* default mask: ff:ff:ff:ff:ff:ff */
for (i = 0; i < ETHER_ADDR_LEN; i++)
mask[i] = 0xff;
}
for (i = 0; i < ETHER_ADDR_LEN; i++)
addr[i] &= mask[i];
ビットマスクが入力さなかった場合ビットマスクの部分は0xffで再度初期化します。
そして指定されたaddrとのAND演算子を取る事でどこまでのビット部分が重要か表現します。
ビットマスクが入力されなかった場合は、ユニークなMACアドレス自体が重要となるので、AND演算子でそのままユニークなMACアドレスが表現できます。
その上で/で表現したビットマスクの長さ表現を見ていきます。
if (p[ptr - optr - 1] == '/') { /* mask len */
long ml = strtol(ptr, &ap, 10);
if (*ap != 0 || ml > ETHER_ADDR_LEN * 8 || ml < 0)
errx(EX_DATAERR, "Incorrect mask length");
for (i = 0; ml > 0 && i < ETHER_ADDR_LEN; ml -= 8, i++)
mask[i] = (ml >= 8) ? 0xff: (~0) << (8 - ml);
/33の場合、残り15bit以外は固有のアドレスでないといけないので0xffで後のAND演算子で固有のアドレス部分を表現できます。
回目以降のループでは、残りの15bitのマスクの表現を判定するために0を反転させたbitを左にシフトします(5回目、6回目で10000000:00000000になる)。
ここら辺も非常に難しいです。もしかしたら間違っているかも。
他にも、調べなきゃいけない所が出てくるかもしれませんが、ひとまずipfwのMAC解析はこのようにしているのですね。
なんか、そもそものipfwコマンドの標準入力部分の解析をどうやっているか調べないといけない気がしてきた
それ以外にも最低限、MACアドレスの指定をどこまで許容しているか調べないといけませんね。
それで、ipfwを使う理由は解決できた?
正直、ARP Spoofingが容易にできることによる脅威さは実感できたのですが、ではこれを今開発しているコンテナホスティングPFに対策として入れるかどうかは微妙な所です。
コンテナホスティングなので、L2内で異なるOrganizationが入る可能性は大なので、そういう意味ではARP Spoofingしやすい環境ではあるとは思います。
ただし、それならOrganizationレベルでセグメント分けてもいいような気がします。というかクラウド上のコンテナホスティングサービスだと、Organizationごとに独立したレベルまで持っていかないとクラウドでやる意味がないとも思っています。実際、libiocもL2レベルのフィルタリングよりも上のレイヤでフィルタリングする予定です(今後)。
とは言え、それまでの繋ぎとしてipfwのPython Wrapperは継続して開発していきます。
下に、まだipfw -a listの結果しか出力できないですが、一応コードを公開しているのでリポジトリを共有します。
https://github.com/himrock922/py-ipfw
まとめ
一通り軽く調べた程度なので、全部深く調べていかないといけないのですが、一応これで最初に何を調べたらいいのか、その登竜門は出来た気がするので、今度執筆する時は一個一個深くやっていきたいと思います。
最後に
ネットワーク難しい。
参考文献
- https://github.com/bsdci/libioc/pull/716
- https://qiita.com/tatsuya4150/items/474b60beed0c04d5d999
- https://tools.ietf.org/html/rfc826
- https://tools.ietf.org/html/rfc1042
- https://tools.ietf.org/html/rfc1122
- https://www.ietf.org/rfc/rfc2979
- https://github.com/freebsd/freebsd/blob/master/sbin/ipfw
- http://support.tenasys.com/intimehelp_5_JP/util_ipfw.html