こちらのipsetによる防御。これを運用して分かったのが

  • /16や/8で切る狂気
  • 自分自身がロックアウトされるという恐怖

の2点。そこで、これを回避するためのスクリプトを改良しました。

#!/bin/bash

# セット名の定義
SET_NAME="ufw-blocklist"
SAVE_FILE="/etc/ufw/ipsets.save"

# 1. root権限チェック
if [[ $EUID -ne 0 ]]; then
   echo "エラー: このスクリプトは sudo または root 権限で実行してください。"
   exit 1
fi

# 保存用関数
save_ipset() {
    read -p "設定をファイルに保存しますか? (y/n): " confirm
    if [[ "$confirm" =~ ^[Yy]$ ]]; then
        ipset save "$SET_NAME" -f "$SAVE_FILE"
        echo "保存完了: $SAVE_FILE"
    else
        echo "保存をスキップしました(メモリ上の設定のみ更新)。"
    fi
}

# ipsetが存在しない場合に作成
if ! ipset list "$SET_NAME" > /dev/null 2>&1; then
    echo "情報: $SET_NAME が見つからないため新規作成します。"
    if ipset create "$SET_NAME" hash:net; then
        echo "作成成功"
    else
        echo "エラー: ipsetの作成に失敗しました。"
        exit 1
    fi
fi

# フェイルセーフ関数:接続中の自分のIP(SSHポータ22)を遮断しようとしていないかチェック
is_ssh_connected_ip() {
    local target_ip=$1
    
    # 現在22番ポートでサスペンド/エスタブリッシュしている接続元のIPをすべて取得
    # (awkで不要なヘッダーやコロン以降のポート番号を削る)
    local my_ssh_ips=$(ss -nt state established sport = :22 | awk 'NR>1 {print $5}' | sed 's/:[^:]*$//' | sort -u)
    
    # テスト対象のターゲットからサブネット部分(/24など)を除外してIPのみにする
    local clean_target=$(echo "$target_ip" | cut -d'/' -f1)

    for ssh_ip in $my_ssh_ips; do
        # 1. 完全一致チェック
        if [[ "$ssh_ip" == "$clean_target" ]]; then
            return 0 # ロックアウトの危険あり
        fi

        # 2. サブネットマスクごとの巻き込みチェック
        if [[ "$target_ip" == */24 ]]; then
            local target_net=$(echo "$clean_target" | cut -d'.' -f1-3)
            local ssh_net=$(echo "$ssh_ip" | cut -d'.' -f1-3)
            if [[ "$target_net" == "$ssh_net" ]]; then
                return 0
            fi
        elif [[ "$target_ip" == */16 ]]; then
            local target_net=$(echo "$clean_target" | cut -d'.' -f1-2)
            local ssh_net=$(echo "$ssh_ip" | cut -d'.' -f1-2)
            if [[ "$target_net" == "$ssh_net" ]]; then
                return 0
            fi
        fi
    done

    return 1 # 安全
}

# IP/NWの形式を整形する関数
format_target() {
    local ip=$1
    echo "=== サブネット選択 ==="
    echo "1) そのまま登録 (${ip})"
    echo "2) /24 で登録 (${ip%.*}.0/24)"
    echo "3) /16 で登録 (${ip%.*.*}.0.0/16)"
    read -p "選択肢を選んでください [1-3]: " subnet_choice
    
    case $subnet_choice in
        2) echo "${ip%.*}.0/24" ;;
        3) echo "${ip%.*.*}.0.0/16" ;;
        *) echo "$ip" ;;
    esac
}

# メインループ
while true; do
    echo "------------------------------------------"
    echo " ipset 管理メニュー ($SET_NAME)"
    echo "------------------------------------------"
    echo "1) IP/ネットワークを追加"
    echo "2) IP/ネットワークを削除"
    echo "3) ステータス表示 (list)"
    echo "q) 終了 (Exit)"
    echo "------------------------------------------"
    read -p "番号を選択してください: " choice

    case $choice in
        1)
            echo "--- 追加モード ---"
            local added_any=false
            while true; do
                read -p "追加する IPアドレス: " raw_ip
                if [ -n "$raw_ip" ]; then
                    # サブネットの選択
                    target=$(format_target "$raw_ip")
                    
                    # フェイルセーフ発動チェック
                    if is_ssh_connected_ip "$target"; then
                        echo "【警告】現在あなたが接続しているSSH元IPが含まれているため、登録をブロックしました! ($target)"
                    else
                        if ipset add "$SET_NAME" "$target" 2>/dev/null; then
                            echo "追加成功: $target"
                            added_any=true
                        else
                            echo "エラー: 追加に失敗したか、既に登録されています。 ($target)"
                        fi
                    fi
                fi
                
                # 続けて入力するか確認
                read -p "続けて他のIPを入力しますか? (y/n): " continue_add
                if [[ ! "$continue_add" =~ ^[Yy]$ ]]; then
                    break
                fi
                echo "------------------------------------------"
            done
            
            # 追加が1件でもあれば一括で保存確認
            if [ "$added_any" = true ]; then
                save_ipset
            fi
            ;;
            
        2)
            echo "--- 削除(解除)モード ---"
            read -p "削除する IPアドレス: " raw_ip
            if [ -n "$raw_ip" ]; then
                target=$(format_target "$raw_ip")
                
                # 削除時も念のためセーフティ(既存の接続を誤って切らないようにするため)
                if is_ssh_connected_ip "$target"; then
                    echo "【警告】現在接続中のSSHセッションが含まれています。削除を中止します。 ($target)"
                else
                    if ipset del "$SET_NAME" "$target" 2>/dev/null; then
                        echo "削除成功: $target"
                        save_ipset
                    else
                        echo "エラー: 削除に失敗したか、リストに存在しません。 ($target)"
                    fi
                fi
            fi
            ;;
            
        3)
            echo "--- 現在のリスト ---"
            ipset list "$SET_NAME"
            ;;
            
        q)
            echo "終了します。"
            exit 0
            ;;
            
        *)
            echo "無効な選択です。"
            ;;
    esac
    echo ""
done

ステップ 1:スクリプトの配置と実行権限の付与

まずはスクリプトをサーバーに保存し、実行できるように権限を付与します。

  • スクリプトファイルの作成: 任意のディレクトリ
  • 任意の場所にファイルを作成します(例: blocklist-mgr.sh)。
  • 実行権限の付与: chmod コマンド
  • sudo chmod +x blocklist-mgr.sh を実行して、スクリプトに実行権限を与えます。
  • root権限で起動: sudo 必須
  • sudo ./blocklist-mgr.sh で起動します。

ステップ 2:メニュー画面の操作

起動すると、以下のような対話型メニューが表示されます。

```text

ipset 管理メニュー (ufw-blocklist)

1) IP/ネットワークを追加
2) IP/ネットワークを削除
3) ステータス表示 (list)

q) 終了 (Exit)

番号を選択してください:

1 IP/ネットワークを追加する場合(メニューで「1」を選択)

悪意のあるアクセス元IPを見つけたら、「1」を入力して追加モードに入ります。

  1. IPアドレスの入力: ブロックしたいIP(例: 192.168.1.50)を入力します。
  2. サブネット(範囲)の選択:以下のように範囲を聞かれるので、攻撃の規模に合わせて選択します。
    • 1(単一IPのみ)
    • 2(/24:周辺のIP 256台をまとめてブロック)
    • 3(/16:大規模なネットワークごとブロック)
  3. 連続入力: 「続けて他のIPを入力しますか? (y/n)」と聞かれるので、複数ある場合は y で続けて入力できます。
  4. ファイルへの永続化:

追加が終わると「設定をファイルに保存しますか? (y/n)」と聞かれます。ここで y を選ぶと、OS再起動後もブロックが有効になります。

もし間違えて「自分が今SSH接続しているIP(またはその所属サブネット)」を入力してしまった場合、スクリプトが自動でそれを検知し、

【警告】登録をブロックしました! と表示して処理を中断します。これにより、リモートワーク中にサーバーから締め出される悲劇を防ぎます。

2 ブロックを解除(削除)する場合(メニューで「2」を選択)

誤ってブロックしてしまったIPや、制限を解除したいIPがある場合は「2」を選択します。
追加時と同様にIPを入力し、サブネット範囲を選択すれば、リストから安全に削除されます。ここでも自分が接続中のIPを誤って削除(変更)しないようセーフティが働きます。

3 現在のブロックリストを確認する場合(メニューで「3」を選択)

「3」を選択すると、現在 ipset に登録されてリアルタイムでブロックされているIPの一覧がズラリと表示されます。正しく反映されているか確認したいときに便利です。

4 終了する場合(メニューで「q」を選択)

「q」を入力すると、安全にメニューを閉じます。