目次

FreeBSDのIPFW2を使う サービス編

2026-05-01
簡易的な IP BAN サービスの例

先にFreeBSDのIPFW2を使う 運用編を参照して大雑把なipfwコマンドの使い方を確認してください。

petitban

プチバンとでも呼んでください。※頭の中では「ぺちっとBAN」って発音になってます

Pyhonスクリプト、Shellスクリプトで構成されています。

リクエスト送信プログラムはデーモンプログラムへWebSocketで接続してリクエストの指示コマンドを送り込みます。
デーモンプログラムはリクエストされた指示コマンドをipfwコマンドのコマンドラインへ変換して実行します。

事前の修正

ファイルは以下に配置した後、修正を行う必要があります。

また、/etc/rc.conf への追記が必要です。

/etc/rc.conf

以下の行を /etc/rc.conf へ追加します。

petitban_daemon_enable="YES"

この行を追加する事で、OS起動時にpetitban_daemon.pyは自動的にデーモンとして起動されるし service コマンドから起動も可能です。
※ごめん、起動はできるけど停止が上手く行ってない。止めるときは kill コマンドでプロセス殺してください。

petitban_daemon.py

このスクリプトは python3 3.11 の利用を想定しているので利用する環境に合わせて変更してください。
待ち受けポート番号は8765です。
※FreeBSDではシンプルに python, python3 コマンドが提供されてない模様。利用できるなら/usr/bin/env とか使ってください。

petitban_daemon.py
#!/usr/local/bin/python3.11
#
# Note:
#   Ubuntu/Debian では /usr/bin/env python が有効だが、
#   FreeBSD (pkg/ports) では python コマンドが提供されないため失敗する。
#   Apache piped logger は PATH が空に近いので、絶対パス指定が必須。
 
import asyncio
import websockets
import subprocess
from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError
 
DAEMONNAME="petitban"
LISTEN="8765"
 
def log_syslog(message):
    # logger コマンドで syslog に出力
    subprocess.run(
        ["logger", "-t", f"{DAEMONNAME}", message],
        check=False
    )
 
async def handler(websocket):
    async for message in websocket:
        instruction = message.strip()
        words       = instruction.split(" ")
 
        if len(words) != 3 :
            raise ValueError(f'bad instraction:{instruction}')
 
        tbl   = words[0].upper()
        act   = words[1].upper()
        ip    = words[2]
 
        try:
            match act:
                case "ADD":
                    subprocess.run(
                        ["ipfw", "table", tbl, "add", ip],
                        check=True,
                        capture_output=True,
                        text=True
                    )
                    log_syslog(f"Added to table {tbl}: {ip}")
 
                case "DEL":
                    subprocess.run(
                        ["ipfw", "table", tbl, "delete", ip],
                        check=True,
                        capture_output=True,
                        text=True
                    )
                    log_syslog(f"Deleted from table {tbl}: {ip}")
 
                case _:
                    log_syslog(f"bad instraction:{words}")
 
        except Exception as e:
            print(f"[{DAEMONNAME}] Error: {e}")
            log_syslog(f"Error: {e}")
 
            try:
                await websocket.send(f"ERROR: {e}")
            except (ConnectionClosedOK, ConnectionClosedError):
                pass
 
async def main():
    print(f"[{DAEMONNAME}] Starting WebSocket server on ws://0.0.0.0:{LISTEN}")
    log_syslog(f"WebSocket daemon started on port {LISTEN}")
    async with websockets.serve(handler, "0.0.0.0", int(LISTEN)):
        await asyncio.Future()  # run forever
 
if __name__ == "__main__":
    asyncio.run(main())

petitban_send.py

このスクリプトは python3 3.11 の利用を想定しているので利用する環境に合わせて変更してください。
待ち受けポート番号は8765です。
※FreeBSDではシンプルに python, python3 コマンドが提供されてない模様。利用できるなら/usr/bin/env とか使ってください。

petitban_send.py
#!/usr/local/bin/python3.11
#
# Note:
#   Ubuntu/Debian では /usr/bin/env python が有効だが、
#   FreeBSD (pkg/ports) では python コマンドが提供されないため失敗する。
#   Apache piped logger は PATH が空に近いので、絶対パス指定が必須。
 
import sys
import asyncio
import websockets
 
LISTENER="8765"
 
async def send(tbl,act,ip):
    uri = f"ws://127.0.0.1:{LISTENER}"
    async with websockets.connect(uri) as ws:
        await ws.send(f'{tbl} {act} {ip}')
 
if __name__ == "__main__":
    tbl = sys.argv[1]
    act = sys.argv[2]
    ip  = sys.argv[3]
    asyncio.run(send(tbl,act,ip))

petitban_wrapper.sh

petitban_send.pyのラッパースクリプト。この処理では標準入力から入力した

"IPアドレス 半角スペース URL"

の1行をIPアドレスとURLに分解して利用しています。
※URLは現在取り込むだけで処理に利用してはいません。

petitban_wrapper.sh
#!/bin/sh
 
bancmd="/usr/local/bin/petitban_send.py"
table="80"
 
while read ip url; do
    ${bancmd} "${table}" ADD "$ip" &
done

petitban_daemon

serviceコマンドから指定されるrcスクリプトです。petitban_daemon.py をデーモンとして起動できます。

petitban_daemon
#!/bin/sh
 
# PROVIDE: petitban_daemon
# REQUIRE: NETWORKING
# KEYWORD: shutdown
 
. /etc/rc.subr
 
name="petitban_daemon"
rcvar="petitban_daemon_enable"
 
pidfile="/var/run/${name}.pid"
command="/usr/sbin/daemon"
command_args="-u root -p ${pidfile} /usr/local/bin/python3.11 /usr/local/bin/petitban_daemon.py"
 
load_rc_config $name
run_rc_command "$1"

Apache24で利用する例

先に petitban_daemon.py を起動して下さい。

service petitban_daemon start

ここはCopilotにゲッソリするほど翻弄されたんですが、FreeBSDのApatchだと素直に外部プログラムを呼び出す事が困難です。
その為、ログ出力をカスタム可能なことに着目し、ログ出力と見せかけて外部コマンドを呼び出す方法を取ります。

httpd.conf

mod_rewrite を有効にしておいて下さい。

LoadModule rewrite_module libexec/apache24/mod_rewrite.so

extra/httpd-ssl.conf

/usr/local/etc/apache24/extra/httpd-ssl.conf にVirtualHostを定義してあるなら、出来るだけ最初の方に

を定義して、その下にCustomLogを定義します。

  <VirtualHost _default_:443>
 
    SSLEngine on
    RewriteEngine On
 
    CustomLog "❘/usr/local/bin/petitban_wrapper.sh" "%a %U%q" env=USE_PETITBAN
 
  :
  :

Apache君は、変数 USE_PETITBAN が定義されている場合に、ログ書き出しプログラムだと思い込んでいる /usr/local/bin/petitban_wrapper.sh へログテキストになる “IPアドレス URL(クエリパラメタ付き)” を渡します。

ログ文字列は以下の書式で生成されます。

.htaccess

ドキュメントルートに .htaccess を配置しておきます。
この中でmod_rewriteを使って参照URLの検査を行い、慎み無い場合には変数 USE_PETITBAN を定義します。
この変数が定義された時に限り /usr/local/bin/petitban_wrapper.sh が呼び出しされます。

.htaccess
RewriteEngine On
RewriteCond %{REQUEST_URI} php/ [NC,OR]
RewriteCond %{REQUEST_URI} /php [NC,OR]
RewriteCond %{REQUEST_URI} \.\. [NC,OR]
RewriteCond %{REQUEST_URI} \.(git|env|bak|exe) [NC,OR]
RewriteCond %{REQUEST_URI} (cgi-bin|owa|oracle|admin|server)/ [NC]
RewriteRule ^ - [E=USE_PETITBAN:1,E=CLIENT_IP:%{REMOTE_ADDR}]

ログ

/var/log/messages にはこんな感じで出力されます。

May  1 13:58:33 vault petitban[1241]: WebSocket daemon started on port 8765
May  1 14:24:21 vault petitban[1344]: Error: Command '['ipfw', 'table', '80', 'add', '176.65.149.253']' returned non-zero exit status 71.
May  1 14:36:33 vault petitban[1365]: Error: Command '['ipfw', 'table', '80', 'add', '65.49.1.202']' returned non-zero exit status 71.
May  1 15:20:46 vault petitban[1662]: Added to table 80: 148.135.54.39

Errorは、既にテーブル登録済みのIPアドレス登録をしようとしたせいです。本来なら2回目のアクセスは発生しないはずなんですがタイミングが何かズレたのでしょう。
とりあえず放置。エラー出さずに無視でもいいんじゃないかなと思いますが改造は各人の責任で。

しばらく稼働させておくと、こんな感じで慎みの無いクライアントのIPアドレスが集まってきます。

root@vault:/usr/local/www/apache24/data # ipfw table 80 list
8.222.225.103/32 0
20.220.233.65/32 0
45.205.1.8/32 0
64.62.156.142/32 0
65.49.1.202/32 0
77.83.39.94/32 0
79.124.40.174/32 0
82.24.64.32/32 0
93.123.109.62/32 0
118.238.5.27/32 0
148.135.54.39/32 0
167.250.224.25/32 0
172.202.118.19/32 0
176.32.193.16/32 0
176.65.149.253/32 0
194.165.16.165/32 0
root@vault:/usr/local/www/apache24/data #

テーブル80とIPFW2フィルタルールとの関係

IPFW2の02000番のフィルタ条件でテーブル80が適用されています。このテーブル80に格納されているIPアドレスが接続拒否対象のIPアドレスになります。

root@vault:/usr/local/www/apache24/data # ipfw list
00100 allow ip from any to any via lo0
00200 deny ip from any to 127.0.0.0/8
00300 deny ip from 127.0.0.0/8 to any
00400 deny ip from any to ::1
00500 deny ip from ::1 to any
00600 allow ipv6-icmp from :: to ff02::/16
00700 allow ipv6-icmp from fe80::/10 to fe80::/10
00800 allow ipv6-icmp from fe80::/10 to ff02::/16
00900 allow ipv6-icmp from any to any icmp6types 1
01000 allow ipv6-icmp from any to any icmp6types 2,135,136
02000 deny ip from table(80) to any
02001 allow tcp from any to me 443 in setup
02002 deny tcp from any to me 443 in tcpflags fin,psh,urg
02003 deny tcp from any to me 443 in tcpflags syn,fin
02004 deny tcp from any to me 443 in tcpflags !syn,!ack,!rst
65000 allow ip from any to any
65535 count ip from any to any not // orphaned dynamic states counter
65535 deny ip from any to any
root@vault:/usr/local/www/apache24/data #