努力したWiki

推敲の足りないメモ書き多数

ユーザ用ツール

サイト用ツール


documents:os:freebsd:freebsd-032

FreeBSDのIPFW2を使う サービス編

2026-05-04 GitHubに公開した
2026-05-03 連続登録時のエラー出力を抑止した
2026-05-02-3 待ち受けを “ws:0.0.0.0:{LISTEN}” → “ws:127.0.0.1:{LISTEN}” に変更
2026-05-02-2 デーモン内コマンド呼び出しを“ipfw”→“/sbin/ipfw”に変更
2026-05-02 スクリプトのアップデート実施
2026-05-01 簡易的な IP BAN サービスの例

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

このページのコードよりも手を入れた版をGitHubで公開しました。

petitban

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

IPFW2のテーブル機能を使って不正アクセスと思われるIPアドレスのアクセスを遮断(BAN)するサービスプログラムです。

IPFW2で定義したテーブルへのipfwコマンド発行をするためのデーモンと、デーモンとの通信クライアントで構成されています。
Apache24で使う際のサンプルとしてラッパースクリプトを用意しています。

  • 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

それぞれのファイルに chmod 755 で実行権限を付けておきます。

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

/etc/rc.conf

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

petitban_daemon_enable="YES"

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

petitban_daemon.py

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

うちの環境では

pkg install py311-websockets-16.0

が必要だったけど、臨機応変に対応してください。

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
import shlex
import signal
import sys
from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError
 
DAEMONNAME  = "petitban"
LISTEN_ADDR = "127.0.0.1"
LISTEN_PORT = "8765"
IPFWCMD     = "/sbin/ipfw"
LAST_ADD_IP = None
 
def log_syslog(message):
    # logger コマンドで syslog に出力
    subprocess.run(
        ["logger", "-t", f"{DAEMONNAME}", message],
        check=False
    )
 
async def handler(websocket):
    global LAST_ADD_IP
    async for message in websocket:
        instruction = message.strip()
        words       = shlex.split(instruction)
 
        if len(words) != 4 :
            raise ValueError(f'bad instruction:{instruction}')
 
        tbl     = words[0].upper()
        act     = words[1].upper()
        ip      = words[2]
        comment = words[3]
 
        try:
            match act:
                case "ADD":
                    if LAST_ADD_IP == ip:
                        continue
                    subprocess.run(
                        [IPFWCMD, "table", tbl, "add", ip],
                        check=True,
                        capture_output=True,
                        text=True
                    )
                    log_syslog(f"Added to table {tbl}: {ip} ,{comment}")
                    LAST_ADD_IP = ip
 
                case "DEL":
                    subprocess.run(
                        [IPFWCMD, "table", tbl, "delete", ip],
                        check=True,
                        capture_output=True,
                        text=True
                    )
                    log_syslog(f"Deleted from table {tbl}: {ip} ,{comment}")
 
                case _:
                    log_syslog(f"bad instruction:{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
 
def handle_sigterm(signum, frame):
    log_syslog("Shutting down petitban daemon (SIGTERM received)")
    sys.exit(0)
 
signal.signal(signal.SIGTERM, handle_sigterm)
 
async def main():
    ##print(f"[{DAEMONNAME}] Starting WebSocket server on ws://{LISTEN_ADDR}:{LISTEN_PORT}")
    log_syslog(f"WebSocket daemon started on addr ws://{LISTEN_ADDR}:{LISTEN_PORT}")
    async with websockets.serve(handler, LISTEN_ADDR, int(LISTEN_PORT)):
        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
 
async def send(tbl,act,ip,comm):
    uri = "ws://127.0.0.1:8765"
    async with websockets.connect(uri) as ws:
        await ws.send(f'{tbl} {act} {ip} "{comm}"')
 
if __name__ == "__main__":
    tbl  = sys.argv[1]
    act  = sys.argv[2]
    ip   = sys.argv[3]
    comm = "MANUAL"
    if len(sys.argv) >= 5 :
        comm = sys.argv[4]
    asyncio.run(send(tbl,act,ip,comm))

petitban_wrapper.sh

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

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

の1行をIPアドレスとURLに分解して利用しています。

petitban_wrapper.sh
#!/bin/sh
 
bancmd="/usr/local/bin/petitban_send.py"
table="80"
 
while read ip url; do
    ${bancmd} "${table}" ADD "$ip" "AUTO,PATH=$url" &
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"

動作テスト

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

service petitban_daemon start

/var/log/messages に petitban の起動メッセージが出ていれば成功です。

root@vault:~ # tail /var/log/messages
May  2 06:15:50 vault pkg[2428]: py311-websockets-16.0 installed
May  2 06:37:03 vault petitban[2734]: WebSocket daemon started on port 8765
root@vault:~ #

適当なダミーのIPアドレスを登録・削除してみます。

root@vault:~ # petitban_send.py 80 add 192.168.192.168
root@vault:~ # ipfw table 80 list | grep 192.168.192.168
192.168.192.168/32 0
root@vault:~ # petitban_send.py 80 del 192.168.192.168
root@vault:~ # ipfw table 80 list | grep 192.168.192.168
root@vault:~ # grep petitban /var/log/messages
May  2 06:37:03 vault petitban[2734]: WebSocket daemon started on port 8765
May  2 06:40:18 vault petitban[2773]: Added to table 80: 192.168.192.168 ,MANUAL
May  2 06:41:22 vault petitban[2795]: Deleted from table 80: 192.168.192.168 ,MANUAL
root@vault:~ #

動作が確認できたらおめでとうございます。

Apache24で利用する例

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

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|php/) [NC,OR]
RewriteCond %{REQUEST_URI} \.\. [NC,OR]
RewriteCond %{REQUEST_URI} \.(git|env|bak|exe) [NC,OR]
RewriteCond %{REQUEST_URI} /bin/[bckz]*sh [NC,OR]
RewriteCond %{REQUEST_URI} /(wp\-|cgi\-bin) [NC,OR]
RewriteCond %{REQUEST_URI} (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 '['/sbin/ipfw', 'table', '80', 'add', '176.65.149.253']' returned non-zero exit status 71.
May  1 14:36:33 vault petitban[1365]: Error: Command '['/sbin/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 ,AUTO,PATH=/www/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php

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

2026-05-03
ログが煩くなるので、同じIPアドレスの登録リクエストが連続で来た場合最初の1回目を登録して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アドレスになります。

ただ、このままだとうっかり自分のPCのアドレスが登録されたりするとその場で締めだされてしまう事になります。
この例なら、02000番のルールより前に必ず自分のPCのアドレスをallowにするルールを入れておきましょう。
※自分からのsshアクセスだけは許す、みたいな感じに。

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 #
documents/os/freebsd/freebsd-032.txt · 最終更新: by k896951

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki