目次
FreeBSDのIPFW2を使う サービス編
2026-05-01
簡易的な IP BAN サービスの例
先にFreeBSDのIPFW2を使う 運用編を参照して大雑把なipfwコマンドの使い方を確認してください。
petitban
プチバンとでも呼んでください。※頭の中では「ぺちっとBAN」って発音になってます
Pyhonスクリプト、Shellスクリプトで構成されています。
- petitban_daemon.py - デーモンプログラム。Pythonスクリプト。rootで実行される。
- petitban_send.py - リクエスト送信プログラム。Pythonスクリプト。デーモンプログラムにリクエストを送信します。
- petitban_wrapper.sh - リクエスト送信プログラム利用例。Shellスクリプト。petitban_send.py のラッパー。
- petitban_daemon - デーモンサービス起動用。Shellスクリプト。petitban_daemon.py をデーモンとして起動するFreeBSD用定義。
リクエスト送信プログラムはデーモンプログラムへWebSocketで接続してリクエストの指示コマンドを送り込みます。
デーモンプログラムはリクエストされた指示コマンドをipfwコマンドのコマンドラインへ変換して実行します。
事前の修正
ファイルは以下に配置した後、修正を行う必要があります。
- petitban_daemon.py → /usr/local/bin/petitban_daemon.py
- petitban_send.py → /usr/local/bin/petitban_send.py
- petitban_wrapper.sh → /usr/local/bin/petitban_wrapper.sh
- petitban_daemon → /usr/local/etc/rc.d/petitban_daemon
また、/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を定義してあるなら、出来るだけ最初の方に
- SSLEngine on
- RewriteEngine On
を定義して、その下に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(クエリパラメタ付き)” を渡します。
ログ文字列は以下の書式で生成されます。
- %a - IPアドレス(REMOTE_ADDR)
- %U - URL
- %q - クエリパラメタ
.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 #
