firewalldのダイレクトルールとフィルター処理について

<a id="index"></a>
## <a href="#index">目次</a>
* [目次](#index)
* [はじめに](#anchor1)
* [ダイレクトルールの設定](#anchor2)
* [chainについて](#anchor3)
* [ruleについて](#anchor4)
* [hashlimitについて](#anchor5)
* [ハッシュテーブルについて](#anchor6)
* [動作確認ログ](#anchor7)
* [クレジットとコストの計算](#anchor8)
* [参考にしたサイト](#anchor9)

<a id="anchor1"></a>
## <a href="#anchor1">はじめに</a>  

CentOS 7で、firewalldの設定をする機会があったので、調べた内容を書いてみます。

調べた内容は、目次の通りです。

今回、私は、firewalldのダイレクトルールを使ったicmpの制御について、
設定方法と、その動作の確認を行いました。

ダイレクトルールの設定方法は、direct.xmlをエディタで編集しています。

firewall-cmdを使った方法は、試していないため、この記事には出てきません。

また、ルールで使用した拡張モジュール、hashlimitについて興味が出たので、
調べられる限り、調べてみました。

<a id="anchor2"></a>
## <a href="#anchor2">ダイレクトルールの設定</a>

ダイレクトルールは、/etc/firewalld/dirext.xmlを作成、編集します。

デフォルトだと存在していなかったので、エディタで新規作成しました。

以下のような内容で、icmpのEcho Requestの制御が可能なことを確認しています。

```
<?xml version="1.0" encoding="utf-8"?>
<direct>
        <chain table="filter" ipv="ipv4" chain="CHECK_ICMP"/>
        <rule priority="0" table="filter" ipv="ipv4" chain="INPUT">-i lo -j ACCEPT</rule>
        <rule priority="10" table="filter" ipv="ipv4" chain="INPUT">-m state --state RELATED,ESTABLISHED -j ACCEPT</rule>
        <rule priority="20" table="filter" ipv="ipv4" chain="INPUT">-p icmp -m icmp --icmp-type 8 -j CHECK_ICMP</rule>
        <rule priority="21" table="filter" ipv="ipv4" chain="INPUT">-p icmp -j ACCEPT</rule>
        <rule priority="100" table="filter" ipv="ipv4" chain="CHECK_ICMP">-p icmp -m icmp --icmp-type 8 -m hashlimit --hashlimit 1/s --hashlimit-burst 3 --hashlimit-mode srcip --hashlimit-name check_icmp --hashlimit-htable-expire 300000 -j RETURN</rule>
        <rule priority="150" table="filter" ipv="ipv4" chain="CHECK_ICMP">-j LOG --log-level debug --log-prefix 'ping_attack: '</rule>
        <rule priority="151" table="filter" ipv="ipv4" chain="CHECK_ICMP">-j DROP</rule>
</direct>
```

/etc/firewalld/direct.xmlを変更した後は、
systemctl restart firewalldを実行して、設定を反映します。

```
[root@localhost ~]# systemctl restart firewalld
[root@localhost ~]#
```

<a id="anchor3"></a>
## <a href="#anchor3">chainについて</a>

先ほどのdirect.xmlの内容では、chainタグを使って、自作のチェイン、CHECK_ICMPを作成しています。
チェインは、処理内容をまとめたものに、名前を付けたもの（関数のようなもの）です。

チェインは、デフォルトで、いくつか存在しています。
filterと呼ばれるtableには、INPUT、FORWARD、OUTPUTの組み込みチェインが存在します。

今回は、受信時のパケットに対してruleを適用したいだけなので、主にINPUTと、自作したchainを使っています。
ipv4のみを見るため、ipv="ipv4"としています。

tableやipvなどの詳細は、firewalldの、ダイレクトルールについてのマニュアルを参照してください。

<a id="anchor4"></a>
## <a href="#anchor4">ruleについて</a>
チェインごとに、ruleを作成できます。
組み込みチェインに対してもルールを追加できます。

filterの組み込みチェインであるINPUTに対して、ruleを追加することで、
受信時のパケットをフィルターする、という設定を行っていきます。

ruleはruleタグで指定します。

タグには、プライオリティと、そのruleの適用先のチェイン（どのtableの、どのipvの、どのチェインか）を指定します。

プライオリティは、0からの番号を指定します。より小さい番号から先に、ルールが処理されていきます。
※同一のプライオリティのruleが存在した場合、その順序は不定のようです。
※チェインごとにプライオリティは独立しているのか？という点は試していません。。。

ruleタグの内容として、パケットのマッチ条件、および動作を指定します。
条件には、そのパケットのプロトコル、送信元、宛先、および拡張モジュールなどがあります。
動作には、ACCEPT、DROP、RETURNなどがあります。
これらの詳細については、iptables、およびiptables-extensionsのマニュアルを見てください。

先のdirect.xmlの内容を例に、ルールの説明を記載します。

```
# インターフェイスがloかチェックする
# マッチしたら許可する
 <rule priority="0" table="filter" ipv="ipv4" chain="INPUT">-i lo -j ACCEPT</rule>
```

```
# 拡張モジュールstateを使用して、パケットの状態がRELATED,ESTABLISHEDをチェックする
# マッチしたら許可する
<rule priority="10" table="filter" ipv="ipv4" chain="INPUT">-m state --state RELATED,ESTABLISHED -j ACCEPT</rule>
```

```
# icmpかチェックする
# 拡張モジュールicmpを使用して、icmp-typeが8（Echo Request）かチェックする
# マッチしたらCHECK_ICMPに移動する
<rule priority="20" table="filter" ipv="ipv4" chain="INPUT">-p icmp -m icmp --icmp-type 8 -j CHECK_ICMP</rule>
```

```
# icmpかチェックする。
# マッチしたら許可する。
# 1つ前の条件と合わせると、icmpのうち、Echo Requestだけ特別なチェックをして、それ以外のicmpは許可する、となる。
<rule priority="21" table="filter" ipv="ipv4" chain="INPUT">-p icmp -j ACCEPT</rule>
```

```
# これはCHECK_ICMPチェインのルール
# icmpかチェックする
# 拡張モジュールicmpを使用して、icmp-typeが8（Echo Request）かチェックする
# 拡張モジュールhashlimitを使用して、秒間パケット数をチェックする
# マッチしたら、元のチェインのルールに戻る（次に実行されるルールは、呼び出し元のルールの、次のルール）
# hashlimitについては後述する
 <rule priority="100" table="filter" ipv="ipv4" chain="CHECK_ICMP">-p icmp -m icmp --icmp-type 8 -m hashlimit --hashlimit 1/s --hashlimit-burst 3 --hashlimit-mode srcip --hashlimit-name check_icmp --hashlimit-htable-expire 300000 -j RETURN</rule>
```

```
# これはCHECK_ICMPチェインのルール
# このルール自体には、マッチ条件は無く、このルールが実行されたら、ログを出力する。
<rule priority="150" table="filter" ipv="ipv4" chain="CHECK_ICMP">-j LOG --log-level debug --log-prefix 'ping_attack: '</rule>
```

```
# これはCHECK_ICMPチェインのルール
# このルール自体には、マッチ条件は無く、このルールが実行されたら、パケットを破棄する。
<rule priority="151" table="filter" ipv="ipv4" chain="CHECK_ICMP">-j DROP</rule>
```

<a id="anchor5"></a>
## <a href="#anchor5">hashlimitについて</a>

拡張モジュール -m hashlimitについて。
この拡張モジュールを使用することで、たとえば、秒間10件のパケットを許可し、
それ以上のパケットが来た場合は、破棄する、というような処理を行うことが出来ます。

-m hashlimitには、以下の引数が必要になります。
--hashlimit 1/s
--hashlimit-burst 3
--hashlimit-mode srcip
--hashlimit-name check_icmp
--hashlimit-htable-expire 300000

* --hashlimit
受信可能なパケット数の指定をします。
指定方法は、秒間パケット数をそのまま指定するか、もしくは、1/sや1/m、1/h、1/d、というように、指定します。
秒間100件を許可したいなら、100/sとします。分間100件許可するなら、100/mとします。
2秒間に1件としたい、という場合、0.5/sというような指定はできませんが、分間30件（30/m）、
というように指定すれば、同等の結果が得られます。


* --hashlimit-burst
この項目で指定する値は、倍率です。n倍、というような意味合いを持ちます。
たとえば、大体平均100/sのパケットが来る環境で、稀に最大300/sまで増えることがあり、
しかし、これを正常として許可したい、というような場合に、hashlimit-burst 3とします。
hashlimit-burstを3にすることで、一時的に通常時の3倍までは、許可を許可するようになります。
そして、仮に300/sという通信が継続した場合は、100/sの通信に制限されます。
※後述するハッシュテーブルの内容を見ると、理解が早まるかと思います。

* --hashlimit-mode
この項目で、何を主キーとして、パケット情報を管理するのかを指定します。
hashlimitでは、送信元IPごとや、宛先ポートごと、というように、
管理情報を複数持つことが出来ます。
管理情報のことを、ハッシュテーブルといいます。
srcipとした場合は、送信元IPアドレス事に、ハッシュテーブルにレコードが記載されます。

* --hashlimit-name
ハッシュテーブルの名前を指定します。
ここで指定した名前で、/proc/net/ipt_hashlimitに、ハッシュテーブルが作成されます。
たとえば、check_icmpを指定した場合は、
/proc/net/ipt_hashlimit/check_icmp
というハッシュテーブルが作成されます。
このハッシュテーブルは、firewalld起動時に、作成されます。
また、起動前にあった情報は、再起動のタイミングで削除されます。

* --hashlimit-htable-expire
ハッシュテーブル中にあるレコードの、有効期限をミリ秒で指定します。
--hashlimit-htable-expire 300000とすれば、該当するパケットをチェックしたときに、
そのレコードの残り時間は300秒にセットされます。
該当するパケットが来なければ、指定した時間が経過したタイミングで、
レコードは削除されます。


<a id="anchor6"></a>
## <a href="#anchor6">ハッシュテーブルについて</a>  

今回の試したdirect.xmlの内容だと、
/proc/net/ipt_hashlimit/check_icmp
というハッシュテーブルが作成されます。

該当するルールが実行されていない状況だと、中身は空です。

ここで、他のホストから、icmpパケットを送ってみます。
すると、以下のように1行追加されます。

```
[root@localhost ~]# cat /proc/net/ipt_hashlimit/check_icmp
296 172.31.34.66:0->0.0.0.0:0 96000 96000 32000
[root@localhost ~]#
```

ハッシュテーブルには、 --hashlimit-modeで指定した単位で、レコードを持ちます。
この内容は、スペース区切りで、5つの内容を持ちます。

* 残り時間
このレコードが存在する残り秒数です。該当するパケットをチェックしたときに、
--hashlimit-htable-expireで指定した秒数に再度セットされます。
0になった場合、該当するレコードは削除されます。

* 主キー
--hashlimit-modeで指定した主キーです。
srcipを指定した場合は、送信元IPアドレスが172.31.34.66だった場合、
172.31.34.66:0->0.0.0.0:0
というような内容が記載されます。

* クレジット（現在値）
hashlimitは、該当するパケットをチェックするとき、1パケットに設定されているコストを、
このクレジットから支払えるかどうかで、真偽を返します。
支払えた場合は真を返し、クレジットからコストを引きます。
支払えなかった場合は、偽を返し、結果、そのルールはマッチしなかった、となります。
ここで、クレジット（現在値）は、秒間32000、増加します。
秒間32000、というのは、1秒間隔で32000増える、というわけではなく、線形に増加し続けます。

* クレジット（最大値）
クレジットの最大値です。これを超えて、クレジット（現在値）が増加することはありません。
クレジットの値は、コスト × hashlimit-burstを計算した値となります。
コストが32000で、hashlimit-burstが3であれば、クレジットの最大値は96000となります。
※場合によって、コスト × hashlimit-burstにならないときもあります。未調査。

* コスト
1パケットを処理するときに、要求される値です。
この値は1/sを32000として、割り切れる場合は計算可能です。
（割り切れない場合は、この計算式では誤差がでます）
--hashlimit 1/s とした場合は、32000 / 1 = 32000となります。
--hashlimit 2/s とした場合は、32000 / 2 = 16000となります。
--hashlimit 1/m とした場合は、32000 * 60 / 1 = 1920000となります。

上記、クレジットの増え方や、コストの計算は、説明をする上で、わかりやすく
1/s = 32000、として行いました。

実際のコストの決定は、どうやら違うようで、たとえば、123/sというhashlimitを指定した場合、
コストは、上記の計算では約260になりますが、実際の動作を確認してみると、259になります。

一応、疑似的に同じような値を導き出す方法は、確認できたのですが、
なんでこうなるのか、は説明できないです。。。

末尾に、実際に決定するクレジットとコストを求めるコードを置いておきます。

<a id="anchor7"></a>
## <a href="#anchor7">動作確認ログ</a>

以下のdirect.xmlを作成して、firewalldを再起動します。

```
[root@localhost ~]# cat /etc/firewalld/direct.xml
<?xml version="1.0" encoding="utf-8"?>
<direct>
        <chain table="filter" ipv="ipv4" chain="CHECK_ICMP"/>
        <rule priority="0" table="filter" ipv="ipv4" chain="INPUT">-i lo -j ACCEPT</rule>
        <rule priority="10" table="filter" ipv="ipv4" chain="INPUT">-m state --state RELATED,ESTABLISHED -j ACCEPT</rule>
        <rule priority="20" table="filter" ipv="ipv4" chain="INPUT">-p icmp -m icmp --icmp-type 8 -j CHECK_ICMP</rule>
        <rule priority="21" table="filter" ipv="ipv4" chain="INPUT">-p icmp -j ACCEPT</rule>
        <rule priority="100" table="filter" ipv="ipv4" chain="CHECK_ICMP">-p icmp -m icmp --icmp-type 8 -m hashlimit --hashlimit 1/s --hashlimit-burst 3 --hashlimit-mode srcip --hashlimit-name check_icmp --hashlimit-htable-expire 300000 -j RETURN</rule>
        <rule priority="150" table="filter" ipv="ipv4" chain="CHECK_ICMP">-j LOG --log-level debug --log-prefix 'ping_attack: '</rule>
        <rule priority="151" table="filter" ipv="ipv4" chain="CHECK_ICMP">-j DROP</rule>
</direct>

[root@localhost ~]#
[root@localhost ~]# systemctl restart firewalld
[root@localhost ~]#
```

別のホストから、firewalldが動作しているホストに対して、pingを実行します。

ここで1つ、予想と違ったので、詳細にicmpの場合のフィルター動作を記載します。

以下のように、pingコマンドで、-iオプションを使ってインターバルを変えて、
1秒間に5発、icmpパケットを送信します。

```
# intervalを0.2秒にして、1秒間に5発程度、icmpパケットを送れば、hashlimitが1/s、burstが3だけど、
# それ以上のパケットが到着するし、hashlimitが偽になって、DROPに落ちるはずだ！
 ping -i 0.2 192.168.0.1
```

この結果は、DROPに落ちることはなく、また、このコマンドを動作させ続けたとしても、
hashlimitでDROPすることは無いです。

実際にpingを実行したログです。

```
# hashlimitが1/s、burstが3なら、seq4のあたりで、DROPに行くんじゃないのか！？
[root@localhost ~]# ping -i 0.2 192.168.0.1
PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data.
64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=0.183 ms
64 bytes from 192.168.0.1: icmp_seq=2 ttl=64 time=0.170 ms
64 bytes from 192.168.0.1: icmp_seq=3 ttl=64 time=0.289 ms
64 bytes from 192.168.0.1: icmp_seq=4 ttl=64 time=0.209 ms
64 bytes from 192.168.0.1: icmp_seq=5 ttl=64 time=0.184 ms
64 bytes from 192.168.0.1: icmp_seq=6 ttl=64 time=0.225 ms
64 bytes from 192.168.0.1: icmp_seq=7 ttl=64 time=0.289 ms
64 bytes from 192.168.0.1: icmp_seq=8 ttl=64 time=0.271 ms
64 bytes from 192.168.0.1: icmp_seq=9 ttl=64 time=0.224 ms
64 bytes from 192.168.0.1: icmp_seq=10 ttl=64 time=0.246 ms
64 bytes from 192.168.0.1: icmp_seq=11 ttl=64 time=0.190 ms
64 bytes from 192.168.0.1: icmp_seq=12 ttl=64 time=0.145 ms
64 bytes from 192.168.0.1: icmp_seq=13 ttl=64 time=0.137 ms
64 bytes from 192.168.0.1: icmp_seq=14 ttl=64 time=0.165 ms
64 bytes from 192.168.0.1: icmp_seq=15 ttl=64 time=0.119 ms
64 bytes from 192.168.0.1: icmp_seq=16 ttl=64 time=0.131 ms
64 bytes from 192.168.0.1: icmp_seq=17 ttl=64 time=0.210 ms
64 bytes from 192.168.0.1: icmp_seq=18 ttl=64 time=0.240 ms
64 bytes from 192.168.0.1: icmp_seq=19 ttl=64 time=0.178 ms
64 bytes from 192.168.0.1: icmp_seq=20 ttl=64 time=0.228 ms
[root@localhost ~]# 
# ずっと成功する！！なんで！？
```

この状況を確認するために、以下のようにdirect.xmlを変更して、
ログを出して確認してみました。

```
[root@localhost ~]# cat /etc/firewalld/direct.xml
<?xml version="1.0" encoding="utf-8"?>
<direct>
        <chain table="filter" ipv="ipv4" chain="CHECK_ICMP"/>
        <rule priority="21" table="filter" ipv="ipv4" chain="INPUT">-j LOG --log-level debug --log-prefix 'check_1: '</rule>
        <rule priority="1" table="filter" ipv="ipv4" chain="INPUT">-i lo -j ACCEPT</rule>
        <rule priority="10" table="filter" ipv="ipv4" chain="INPUT">-m state --state RELATED,ESTABLISHED -j ACCEPT</rule>
        <rule priority="21" table="filter" ipv="ipv4" chain="INPUT">-j LOG --log-level debug --log-prefix 'check_1: '</rule>
        <rule priority="20" table="filter" ipv="ipv4" chain="INPUT">-p icmp -m icmp --icmp-type 8 -j CHECK_ICMP</rule>
        <rule priority="21" table="filter" ipv="ipv4" chain="INPUT">-j LOG --log-level debug --log-prefix 'check_2: '</rule>
        <rule priority="22" table="filter" ipv="ipv4" chain="INPUT">-p icmp -j ACCEPT</rule>
        <rule priority="100" table="filter" ipv="ipv4" chain="CHECK_ICMP">-p icmp -m icmp --icmp-type 8 -m hashlimit --hashlimit 1/s --hashlimit-burst 3 --hashlimit-mode srcip --hashlimit-name check_icmp --hashlimit-htable-expire 300000 -j RETURN</rule>
        <rule priority="150" table="filter" ipv="ipv4" chain="CHECK_ICMP">-j LOG --log-level debug --log-prefix 'ping_attack: '</rule>
        <rule priority="151" table="filter" ipv="ipv4" chain="CHECK_ICMP">-j DROP</rule>
</direct>

[root@localhost ~]#
[root@localhost ~]# systemctl restart firewalld
[root@localhost ~]#
```

この状態で、先と同じようにpingを実行すると、
kernのログに、以下の内容が出てきます。

```
# macは、一応隠します。
Dec  4 12:26:35 192.168.0.1 kernel: [1042210.642801] check_0: IN=eth0 OUT= MAC=xxx SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=54084 SEQ=1
Dec  4 12:26:35 192.168.0.1 kernel: [1042210.642839] check_1: IN=eth0 OUT= MAC=xxx SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=54084 SEQ=1
Dec  4 12:26:35 192.168.0.1 kernel: [1042210.642869] check_2: IN=eth0 OUT= MAC=xxx SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=54084 SEQ=1
```

別のホストから、0.2秒間隔で、icmpパケットを何発か送っていますが、このログしか出てきません。

どうやら、icmpの場合、同じicmp_idのicmpパケットは、最初のパケットのみしか、INPUTチェインに入ってこないようです。
このため、たとえば、hashlimitを1/sにして、burstを1にして、
他のホストからpingコマンドで、ping -i 0.1 というようにして、icmpパケットをたくさん送ったとしても、
icmp_idが変わらない限り、pingは成功し続けます。

手でpingを、別プロセスで実行し続けるのは大変なので、実験用に、
以下のようなスクリプトを使用します。

```perl:
#! /usr/bin/perl -w

use strict;
use warnings;

$SIG{'CHLD'} = 'IGNORE';


while (1) {
        my $pid = fork();

        if ($pid == 0) {
                system("ping 192.168.0.1 -c 1");
                exit(0);
        }

        select undef, undef, undef, 0.2;
}
```

このスクリプトを別のホスト上で実行しておき、
firewalldが動作しているホスト上でtcpdumpを使ってみると、
以下のような結果が得られます。

```
[root@localhost ~]# tcpdump icmp and host 192.168.0.2
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
12:50:30.916493 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 11340, seq 1, length 64
12:50:30.916553 IP 192.168.0.1 > 192.168.0.2: ICMP echo reply, id 11340, seq 1, length 64
12:50:31.117352 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 11852, seq 1, length 64
12:50:31.117411 IP 192.168.0.1 > 192.168.0.2: ICMP echo reply, id 11852, seq 1, length 64
12:50:31.317484 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 12364, seq 1, length 64
12:50:31.317522 IP 192.168.0.1 > 192.168.0.2: ICMP echo reply, id 12364, seq 1, length 64
12:50:31.518332 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 12876, seq 1, length 64
12:50:31.718706 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 13388, seq 1, length 64
12:50:31.918752 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 13900, seq 1, length 64
12:50:31.918785 IP 192.168.0.1 > 192.168.0.2: ICMP echo reply, id 13900, seq 1, length 64
12:50:32.119558 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 14412, seq 1, length 64
12:50:32.320119 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 14924, seq 1, length 64
12:50:32.520330 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 15436, seq 1, length 64
12:50:32.721219 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 15948, seq 1, length 64
12:50:32.921632 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 16460, seq 1, length 64
12:50:32.921681 IP 192.168.0.1 > 192.168.0.2: ICMP echo reply, id 16460, seq 1, length 64
12:50:33.122178 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 16972, seq 1, length 64
12:50:33.322561 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 17484, seq 1, length 64
12:50:33.523171 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 17996, seq 1, length 64
12:50:33.723931 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 18508, seq 1, length 64
12:50:33.924387 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 19020, seq 1, length 64
12:50:33.924453 IP 192.168.0.1 > 192.168.0.2: ICMP echo reply, id 19020, seq 1, length 64
12:50:34.124944 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 19532, seq 1, length 64
12:50:34.325677 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 20044, seq 1, length 64
12:50:34.526117 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 20556, seq 1, length 64
12:50:34.726403 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 21580, seq 1, length 64
12:50:34.927007 IP 192.168.0.2 > 192.168.0.1: ICMP echo request, id 22092, seq 1, length 64
```

echo replyの行に注目してください。
最初の3発分まで、echo replyを返していますが、
その次のecho replyを返すのが、だいたい1秒間隔になっているのが、わかると思います。

この状態の、kernには、hashlimitが偽になることで実行されるLOGの内容が出てきています。

```
Dec  4 12:50:31 192.168.0.1 kernel: [1043646.212480] ping_attack: IN=eth0 OUT= MAC=xxx SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=12876 SEQ=1
Dec  4 12:50:31 192.168.0.1 kernel: [1043646.412856] ping_attack: IN=eth0 OUT= MAC=xxx SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=13388 SEQ=1
Dec  4 12:50:32 192.168.0.1 kernel: [1043646.813714] ping_attack: IN=eth0 OUT= MAC=xxx SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=14412 SEQ=1
Dec  4 12:50:32 192.168.0.1 kernel: [1043647.014263] ping_attack: IN=eth0 OUT= MAC=xxx SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=14924 SEQ=1
Dec  4 12:50:32 192.168.0.1 kernel: [1043647.214480] ping_attack: IN=eth0 OUT= MAC=xxx SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=15436 SEQ=1
Dec  4 12:50:32 192.168.0.1 kernel: [1043647.415358] ping_attack: IN=eth0 OUT= MAC=xxx SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=15948 SEQ=1
Dec  4 12:50:33 192.168.0.1 kernel: [1043647.816288] ping_attack: IN=eth0 OUT= MAC=xxx SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=16972 SEQ=1
Dec  4 12:50:33 192.168.0.1 kernel: [1043648.016702] ping_attack: IN=eth0 OUT= MAC=xxx SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=17484 SEQ=1
Dec  4 12:50:33 192.168.0.1 kernel: [1043648.217317] ping_attack: IN=eth0 OUT= MAC=xxx SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=17996 SEQ=1
Dec  4 12:50:33 192.168.0.1 kernel: [1043648.418077] ping_attack: IN=eth0 OUT= MAC=xxx SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=18508 SEQ=1
Dec  4 12:50:34 192.168.0.1 kernel: [1043648.819083] ping_attack: IN=eth0 OUT= MAC=xxx SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=19532 SEQ=1
Dec  4 12:50:34 192.168.0.1 kernel: [1043649.019816] ping_attack: IN=eth0 OUT= MAC=xxx SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=20044 SEQ=1
Dec  4 12:50:34 192.168.0.1 kernel: [1043649.220218] ping_attack: IN=eth0 OUT= MAC=xxx SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=20556 SEQ=1
Dec  4 12:50:34 192.168.0.1 kernel: [1043649.420528] ping_attack: IN=eth0 OUT= MAC=xxx SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=21580 SEQ=1
```

また、この状態でハッシュテーブルの中身を、catなどを連打して表示させると、
以下のようになります。

```
[root@localhost ~]# cat /proc/net/ipt_hashlimit/check_icmp
299 192.168.0.2:0->0.0.0.0:0 25216 96000 32000
[root@localhost ~]# cat /proc/net/ipt_hashlimit/check_icmp
299 192.168.0.2:0->0.0.0.0:0 1184 96000 32000
[root@localhost ~]# cat /proc/net/ipt_hashlimit/check_icmp
299 192.168.0.2:0->0.0.0.0:0 8768 96000 32000
[root@localhost ~]# cat /proc/net/ipt_hashlimit/check_icmp
299 192.168.0.2:0->0.0.0.0:0 16064 96000 32000
[root@localhost ~]# cat /proc/net/ipt_hashlimit/check_icmp
299 192.168.0.2:0->0.0.0.0:0 23936 96000 32000
[root@localhost ~]# cat /proc/net/ipt_hashlimit/check_icmp
299 192.168.0.2:0->0.0.0.0:0 31424 96000 32000
[root@localhost ~]# cat /proc/net/ipt_hashlimit/check_icmp
299 192.168.0.2:0->0.0.0.0:0 8768 96000 32000
[root@localhost ~]# cat /proc/net/ipt_hashlimit/check_icmp
299 192.168.0.2:0->0.0.0.0:0 15840 96000 32000
[root@localhost ~]# cat /proc/net/ipt_hashlimit/check_icmp
299 192.168.0.2:0->0.0.0.0:0 24608 96000 32000
[root@localhost ~]# cat /proc/net/ipt_hashlimit/check_icmp
299 192.168.0.2:0->0.0.0.0:0 32672 96000 32000
[root@localhost ~]# cat /proc/net/ipt_hashlimit/check_icmp
299 192.168.0.2:0->0.0.0.0:0 8672 96000 32000
[root@localhost ~]# cat /proc/net/ipt_hashlimit/check_icmp
299 192.168.0.2:0->0.0.0.0:0 16800 96000 32000
[root@localhost ~]# cat /proc/net/ipt_hashlimit/check_icmp
299 192.168.0.2:0->0.0.0.0:0 25024 96000 32000
[root@localhost ~]# cat /proc/net/ipt_hashlimit/check_icmp
299 192.168.0.2:0->0.0.0.0:0 32288 96000 32000
[root@localhost ~]# cat /proc/net/ipt_hashlimit/check_icmp
299 192.168.0.2:0->0.0.0.0:0 7456 96000 32000
[root@localhost ~]# cat /proc/net/ipt_hashlimit/check_icmp
299 192.168.0.2:0->0.0.0.0:0 14976 96000 32000
```

最初の要素の、299が変動していないのは、0.2秒間隔で、常にパケットが来ているためです。
クレジット（現在値）に注目してください。
1パケットのコストである32000未満の状態では、徐々に増え続けて、
そして32000以上になったとき、hashlimitの条件は真となり、パケットがACCEPTされます。
そしてクレジット（現在値）はコスト分引かれて小さくなり、これを繰り返しています。
最初の3発分は、クレジットの最大値が3倍分あり、そこから値を引くため、連続で3回までは、
コストを支払うことができるので、hashlimitは真となります。先のtcpdumpの結果からも、
この動作は確認できています。

これまでの内容から、hashlimitがどういう風に動作しているのか、理解できたと思います。


<a id="anchor8"></a>
## <a href="#anchor8">クレジットとコストの計算</a>

hashlimitのコストの計算で、1/s = 32000として説明しましたが、
実際とは異なるという説明をしました。

ここに、実際に使用される内容と、だいたい同じになるように
計算した値を返す、ソースコードを記載します。

```c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int parse_hashlimit(char *hashlimit, char *burst, size_t *cost, size_t *credit) {
        size_t unit = 1;
        size_t v = strtoul(hashlimit, NULL, 10);
        size_t b = strtoul(burst, NULL, 10);

        if (v == 0 || b == 0) {
                return 1;
        }

        char *c = strchr(hashlimit, '/');

        if (c != NULL && c+1 != NULL && *(c+1) != '\0') {
                switch (*(c+1)) {
                        case 's' : unit = 1; break;
                        case 'm' : unit = 60; break;
                        case 'h' : unit = 60 * 60; break;
                        case 'd' : unit = 60 * 60 * 24; break;
                        default : return 1;
                }
        }

        double tmp = 10000.0 / ((double)v / (double)unit);
        double tmp2 = (double)((size_t)tmp) / 10.0 * 32.0;

        *cost = (size_t)tmp2;
        *credit = (size_t)(tmp2 * (double)b);

        return 0;
}

int main(int argc, char** argv) {
        if (argc != 3) {
                printf("Usage: ./a.out hashlimit hashlimit-burst\n");
                exit(1);
        }

        size_t cost = 0;
        size_t credit = 0;

        if (parse_hashlimit(argv[1], argv[2], &cost, &credit)) {
                printf("hashlimit parse error.\n");
                exit(1);
        }

        printf("credit=%lu, cost=%lu\n", credit, cost);
}

```

これをコンパイルして、実行した結果です。

```
[root@localhost ~]# gcc a.c
[root@localhost ~]# ./a.out 1/s 1
credit=32000, cost=32000
[root@localhost ~]# ./a.out 2/s 1
credit=16000, cost=16000
[root@localhost ~]# ./a.out 3/s 1
credit=10665, cost=10665
[root@localhost ~]# ./a.out 4/s 1
credit=8000, cost=8000
[root@localhost ~]# ./a.out 5/s 1
credit=6400, cost=6400
[root@localhost ~]# ./a.out 123/s 1
credit=259, cost=259
[root@localhost ~]# ./a.out 124/s 1
credit=256, cost=256
[root@localhost ~]# ./a.out 123/s 10
credit=2592, cost=259
[root@localhost ~]# ./a.out 124/s 10
credit=2560, cost=256
```

都度、direct.xmlを書き換えて、hashlimitをチェックさせて、
ハッシュテーブルを見る、という作業が面倒なので、
ここではログは割愛します。一応手で試して動作を見ていた限り、
上記の内容はあっていました。

上記以外の内容で、誤差が出るかもしれませんが、、、。
そもそもの実際の計算方法がわかれば、、、という気持ちです。

<a id="anchor9"></a>
## <a href="#anchor9">参考にしたサイト</a>

* firewalldのダイレクトルールについてのマニュアル
https://firewalld.org/documentation/man-pages/firewalld.direct.html

* iptablesのマニュアル
https://linuxjm.osdn.jp/html/iptables/man8/iptables.8.html

* iptables-extensions（拡張モジュール）のマニュアル
https://linuxjm.osdn.jp/html/iptables/man8/iptables-extensions.8.html