From fef485c6d44fff634cc5b2219c351bbbdd2a9086 Mon Sep 17 00:00:00 2001 From: joe Date: Thu, 5 Mar 2026 21:37:53 +0900 Subject: [PATCH] =?UTF-8?q?fail2ban=E5=AF=BE=E7=AD=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 127 +++++++++++++++++++++++++++++++++++++-------------- ipf | 133 +++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 194 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 3c13665..6c2d364 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,108 @@ -このプロジェクトは、nftablesのルールを編集するためのスクリプトです。以下が使用方法とライセンス情報です。 +# ipf - nftables ベースのポートフォワーディング管理ツール + +完全機能な IP フォワーダーで、マルチプロトコル検出(SSH/HTTP/HTTPS など)、日本語・英語ローカリゼーションに対応。 --- -### 概要 -このツールは、`nftables`のルールを編集する際のスクリプトです。 +## 概要 +`nftables` のルールを編集するスクリプトです。IP フォワーディングを管理し、fail2ban などのセキュリティツールと整合した設定を実現。 --- -### 使用方法 -**ルールの編集** - ```bash -使用法: - ipf [オプション] [ルール] +## セキュリティポリシー: fail2ban 対策 -ルール形式: - <ローカルポート>:<ターゲットIP>:<ターゲットポート> - 例: ipf 8080:10.10.100.1:80 (外部アクセス8080を内部80へ) +ipf は転送先のセキュリティ(fail2ban など)との整合性を考慮した設計をしていませんが、以下のように実装可能です。 - <ローカルポート>:<ターゲットポート> - 例: ipf 80:8080 (外部80をローカル8080へ) +### 環境別挙動比較表 - <ポート番号> - 例: ipf 22 (外部22をローカル22へ) +| 環境 | デフォルト動作 | SNAT モード (`-F` オプション) | fail2ban 推奨設定 | +|------|---------------|-------------------------------|------------------| +| **リアルマシン/VM** | MASQUERADE(ホスト IP で中継) | ✅ SNAT でクライアント IP 保持 | `-F` を使用 | +| **コンテナ環境** | MASQUERADE(ホスト IP で中継) | ⚠️ スタンダードな動作 | デフォルトで OK | -オプション: - -l, -L 現在の転送ルールを一覧表示 (引数なしのデフォルト) - -d 指定したハンドルIDまたは :ポート でルールを削除 - 例: ipf -d 12 / ipf -d :80 - -d all すべてのルールを削除してテーブルを初期化 - -t 指定したターゲットの接続性とプロトコルをテスト - -t 既存ルールのハンドルIDを指定して疎通テストを実行 - -R 設定リセット(テーブルを再作成) - -f, -y 確認なしで実行し、IPフォワーディングを強制有効化 - -q クワイエットモード(メッセージ出力を抑制) - -h, --help この詳細ヘルプを表示 - ``` +### SNAT と MASQUERADE の違い +```bash +# MASQUERADE: ホスト IP に置換(シンプル) +nft insert rule inet ipf postrouting daddr "$tip" ... masquerade comment "simple" + +# SNAT (orig): クライアント IP を保持(fail2ban 対応) +nft insert rule inet ipf postrouting daddr "$tip" ... snat to type nat orig comment "client-ip" +``` + +#### 各手法のメリット・デメリット + +| 手法 | メリット | デメリット | +|------|----------|-----------| +| **MASQUERADE**(デフォルト) | シンプル、パフォーマンス良好 | fail2ban でクライアント IP を正しく検出できない | +| **SNAT (orig)** (`-F` オプション) | fail2ban と整合し、クライアント IP が保持される | スケジューリング処理が発生し、少し遅い(現代の CPU では差は微々たるもの) | --- -### ライセンス +## 使用方法 + +**基本コマンド:** +```bash +# ポート転送ルールを追加(例:外部 8080 を内部 80 へ) +ipf 8080:10.10.100.1:80 + +# 失敗のリスクを減らすため、デフォルトでは MASQUERADE を使用します +``` + +**ルール形式:** +- `<ローカルポート>:<ターゲット IP>:<ターゲットポート>` + 例:`ipf 8080:10.10.100.1:80`(外部アクセス 8080 を内部 80 へ) +- `<ローカルポート>:<ターゲットポート>` + 例:`ipf 80:8080`(外部 80 をローカル 8080 へ、ターゲットは 127.0.0.1) +- `<ポート番号>` + 例:`ipf 22`(外部 22 をローカル 22 へ、ターゲットは 127.0.0.1) + +--- + +## オプション + +| オプション | 説明 | 例 | +|-----------|------|-----| +| `-l, -L` | 現在の転送ルールを一覧表示 | `ipf -l` | +| `-d ` | 指定したハンドル ID または : ポートでルールを削除 | `ipf -d 12` / `ipf -d :80` | +| `-d all` | すべてのルールを削除してテーブルを初期化 | `ipf -d all` | +| `-t ` | 指定したターゲットの接続性とプロトコルをテスト | `ipf -t 10.10.100.1:80` | +| `-t ` | 既存ルールのハンドル ID を指定して疎通テストを実行 | `ipf -t 12345678` | +| `-u` | UDP プロトコルを使用(デフォルトは TCP)
ルール追加時:`ipf -u 5353:10.0.0.1:53`
テスト時:`ipf -u -t 10.0.0.1:53` | `ipf -u 5353:10.0.0.1:53` | +| `-R` | 設定リセット(テーブルを再作成) | `ipf -R` | +| `-f, -y` | 確認なしで実行し、IP フォワーディングを強制有効化 | `ipf -f` | +| `-q` | クワイエットモード(メッセージ出力を抑制) | `ipf -q` | +| `-h, --help` | この詳細ヘルプを表示 | `ipf -h` | + +--- + +## セキュリティへの配慮 + +### fail2ban との整合性 + +転送先に fail2ban が稼働している場合、`masquerade` を使用するとクライアント IP がログされません。以下の方法で回避します: + +1. **デフォルト設定(MASQUERADE)**: シンプルで無難 +2. **`-F` オプション**: クライアント IP を保持し、fail2ban と整合させる + +**失敗のリスク低減のため、デフォルトでは MASQUERADE を使用します。** + +### 注意事項 + +- ipf は fail2ban のログを正しく生成することを前提としていません。 +- SNAT モード(`-F` オプション)を使用する場合のみ、クライアント IP が保持されます。 +- コンテナ環境では、MASQUERADE で動作することを確認してください。 + +--- + +## 補足 + +このスクリプトは `qwen3/gpt-oss/gemini-2.5-pro` と `krasherjoe` によって作成されました。 + +--- + +## ライセンス + MIT License に基づくライセンスです。 **権利者**: krasherjoe -**日付**: 2026-01-22 - ---- - -### 補足 -このスクリプトは、`qwen3/gpt-oss/gemini-2.5-pro`と`krasherjoe`によって作成されました。 -AIにリファクタさせたら原型を留めないのが良いのか悪いのか謎。 +**日付**: 2026-01-22 \ No newline at end of file diff --git a/ipf b/ipf index 5d70e84..55e88c7 100755 --- a/ipf +++ b/ipf @@ -1,7 +1,7 @@ #!/bin/bash # Name: ipf -# Version: 1.6.9 -# Date: 2026-02-04 +# Version: 1.7.0 +# Date: 2026-03-05 # Description: Fully featured port forwarder with rich help, multi-protocol # detection (SSH/HTTP/HTTPS/etc.), and JP/EN localization. @@ -14,18 +14,20 @@ declare -A MSGS if [[ "$SYSTEM_LANG" == "ja" ]]; then MSGS=( [system]="\e[34m[システム]\e[0m" - [enabling_fwd]="IPフォワーディングを有効化しています..." - [err_fwd]="致命的: net.ipv4.ip_forward を有効にできませんでした。" - [rules_header]="転送ルール一覧 (テーブル: %s):" + [enabling_fwd]="IP フォワーディングを有効化しています..." + [err_fwd]="致命的:net.ipv4.ip_forward を有効にできませんでした。" + [rules_header]="転送ルール一覧 (テーブル:%s):" [overwrite]="\e[33mポート :%s (ハンドル %s) の既存ルールを上書きします...\e[0m" - [reset_warn]="\e[33m警告: テーブル '%s' 内の全ルールが削除されます。\e[0m" - [confirm]="削除を確認しますか? (y/N): " + [reset_warn]="\e[33m警告:テーブル '%s' 内の全ルールが削除されます。\e[0m" + [confirm]="削除を確認しますか?(y/N): " [reset_done]="テーブル '%s' をリセットしました。" [testing]="ハンドル %s をテスト中 (Local :%s -> Target %s:%s) [%s]... " - [passed]=" \e[32m成功 (パケット増分: +%s)\e[0m" + [passed]=" \e[32m成功 (パケット増分: +%s)\e[0m" [skipped]=" \e[33mスキップ (疎通なし、またはシャドウイングの可能性)\e[0m" [err_not_found]="ハンドル %s が見つかりません。" [unknown_opt]="不明なオプション '%s' です。-h でヘルプを表示してください。" + [nat_mode]="\e[32mSNAT モード (fail2ban 対応)\e[0m" + [masq_mode]="\e[33mMASQUERADE モード (シンプル)\e[0m" ) else MSGS=( @@ -42,6 +44,8 @@ else [skipped]=" \e[33mSKIPPED (No traffic or shadowed)\e[0m" [err_not_found]="Handle %s not found." [unknown_opt]="Unknown option '%s'. Use -h for help." + [nat_mode]="\e[32mSNAT mode (fail2ban compatible)\e[0m" + [masq_mode]="\e[33mMASQUERADE mode (simple)\e[0m" ) fi @@ -53,12 +57,13 @@ if [ "$(id -u)" -ne 0 ]; then fi TABLE_NAME="ipf" -VERSION="1.6.9" +VERSION="1.7.0" PROTO="tcp" FORCE_FLAG=false SKIP_TEST=false QUIET_MODE=false RESET_MODE=false +SNAT_FLAG=false # ----------------------------------------------------------------------------- # 3. Core Functions @@ -84,7 +89,18 @@ init_nft() { } test_connection() { - local host=$1; local port=$2; local info="" + local host=$1; local port=$2; local proto=${3:-$PROTO}; local info="" + + # UDP mode: use nc -zu for connectivity check + if [[ "$proto" == "udp" ]]; then + if nc -zu -w 1 "$host" "$port" >/dev/null 2>&1; then + echo -e "\e[32mUP\e[0m (UDP)" + return 0 + else + echo -e "\e[31mDOWN\e[0m (UDP)" + return 1 + fi + fi # A. Protocol Banner Check (SSH, FTP, etc.) local banner=$(timeout 0.5s nc -w 1 "$host" "$port" 2>/dev/null | head -n 1 | tr -d '\000-\031') @@ -109,7 +125,7 @@ test_connection() { local url="${sch}://${host}:${port}" local res=$(curl -sLk -m 3 -A "Mozilla/5.0 ipf-tester" "${url}/api/tags" 2>/dev/null) - # Ollama API 判定 (JSONに "models" が含まれているか) + # Ollama API 判定 (JSON に "models" が含まれているか) if [[ "$res" == *"models"* ]]; then info=" (Ollama API)" echo -e "\e[32mUP\e[0m${info}" @@ -125,7 +141,7 @@ test_connection() { return 0 fi - # 通常のHTTP/HTTPSステータス判定 + # 通常の HTTP/HTTPS ステータス判定 local code=$(curl -IsLk -m 2 -o /dev/null -w "%{http_code}" "${url}/" 2>/dev/null) [[ "$code" == "000" ]] && code=$(curl -sLk -m 2 -o /dev/null -w "%{http_code}" "${url}/" 2>/dev/null) @@ -146,32 +162,47 @@ test_connection() { show_help() { if [[ "$SYSTEM_LANG" == "ja" ]]; then cat << EOF -ipf (IP Forwarder) - nftablesベースの簡易ポート転送管理ツール +ipf (IP Forwarder) - nftables ベースのポート転送管理ツール 使用法: ipf [オプション] [ルール] ルール形式: - <ローカルポート>:<ターゲットIP>:<ターゲットポート> - 例: ipf 8080:10.10.100.1:80 (外部アクセス8080を内部80へ) + <ローカルポート>:<ターゲット IP>:<ターゲットポート> + 例:ipf 8080:10.10.100.1:80 (外部アクセス 8080 を内部 80 へ) <ローカルポート>:<ターゲットポート> - 例: ipf 80:8080 (外部80をローカル8080へ) + 例:ipf 80:8080 (外部 80 をローカル 8080 へ) <ポート番号> - 例: ipf 22 (外部22をローカル22へ) + 例:ipf 22 (外部 22 をローカル 22 へ) オプション: -l, -L 現在の転送ルールを一覧表示 (引数なしのデフォルト) - -d 指定したハンドルIDまたは :ポート でルールを削除 - 例: ipf -d 12 / ipf -d :80 + -d 指定したハンドル ID または : ポート でルールを削除 + 例:ipf -d 12 / ipf -d :80 -d all すべてのルールを削除してテーブルを初期化 -t 指定したターゲットの接続性とプロトコルをテスト - -t 既存ルールのハンドルIDを指定して疎通テストを実行 - -R 設定リセット(テーブルを再作成) - -f, -y 確認なしで実行し、IPフォワーディングを強制有効化 - -q クワイエットモード(メッセージ出力を抑制) - -h, --help この詳細ヘルプを表示 + -t 既存ルールのハンドル ID を指定して疎通テストを実行 + -u UDP プロトコルを使用 (デフォルトは TCP) + ルール追加時:ipf -u 5353:10.0.0.1:53 + テスト時: ipf -u -t 10.0.0.1:53 + -R 設定リセット (テーブルを再作成) + -f, -y 確認なしで実行し、IP フォワーディングを強制有効化 + -q クワイエットモード (メッセージ出力を抑制) + -F SNAT モード (fail2ban と整合): snat to type nat orig + 例:ipf -F 8080:10.10.100.1:80 + -h, --help この詳細ヘルプを表示 + +セキュリティポリシー: + デフォルトは MASQUERADE モード (シンプル、無難) です。 + fail2ban と整合させるには -F オプションを使用します。 + + SNAT モードの特徴: + - クライアント IP がログされる + - fail2ban が正しく動作する + - パフォーマンスは MASQUERADE よりわずかに低速 + EOF else cat << EOF @@ -195,8 +226,25 @@ Options: -d Delete rule by handle or :port -d all Flush all rules -t Test connectivity/protocol + -u Use UDP protocol (default: TCP) + Add rule: ipf -u 5353:10.0.0.1:53 + Test: ipf -u -t 10.0.0.1:53 -R Reset table - -h, --help Show this help + -f, -y Force enable forwarding + -q Quiet mode (suppress output) + -F SNAT mode (fail2ban compatible): snat to type nat orig + Ex: ipf -F 8080:10.10.100.1:80 + -h, --help Show this help + +Security Policy: + Default is MASQUERADE mode (simple, safe). + Use -F option for fail2ban compatibility. + +SNAT Mode Features: + - Client IP is logged + - fail2ban works correctly + - Slightly slower than MASQUERADE + EOF fi } @@ -231,10 +279,14 @@ test_strict_handle() { local tp=$(echo "$target" | cut -d':' -f2) local uuid=$(echo "$info" | grep -o 'ipf-id:[a-z0-9-]*') local short_id=$(echo "$uuid" | cut -d':' -f2 | cut -c1-8) + # ルールからプロトコルを検出 (明示的に -u 指定がなければルール自体から判定) + local rule_proto=$PROTO + echo "$info" | grep -q "udp" && rule_proto="udp" printf "${MSGS[testing]}" "$h" "$lp" "$tip" "$tp" "$short_id" local count_before=$(nft list table inet "${TABLE_NAME}" 2>/dev/null | grep "$uuid" | grep -o 'packets [0-9]*' | awk '{sum+=$2} END {print sum+0}') - test_connection "$tip" "$tp" - nc -z -w 1 127.0.0.1 "$lp" >/dev/null 2>&1; sleep 0.1 + test_connection "$tip" "$tp" "$rule_proto" + local nc_opt="-z"; [[ "$rule_proto" == "udp" ]] && nc_opt="-zu" + nc $nc_opt -w 1 127.0.0.1 "$lp" >/dev/null 2>&1; sleep 0.1 local count_after=$(nft list table inet "${TABLE_NAME}" 2>/dev/null | grep "$uuid" | grep -o 'packets [0-9]*' | awk '{sum+=$2} END {print sum+0}') if (( count_after > count_before )); then printf "${MSGS[passed]}\n" "$((count_after - count_before))" else printf "${MSGS[skipped]}\n"; fi @@ -261,6 +313,7 @@ list_rules() { add_rule() { local raw=$1; init_nft [[ "$FORCE_FLAG" == true ]] && enable_forwarding + if [[ "$raw" =~ ^([0-9]+):([0-9\.]+):([0-9]+)$ ]]; then lp=${BASH_REMATCH[1]}; tip=${BASH_REMATCH[2]}; tp=${BASH_REMATCH[3]} elif [[ "$raw" =~ ^([0-9]+):([0-9]+)$ ]]; then @@ -268,15 +321,29 @@ add_rule() { elif [[ "$raw" =~ ^([0-9]+)$ ]]; then lp=${BASH_REMATCH[1]}; tip="127.0.0.1"; tp=${BASH_REMATCH[1]} else err "Invalid format: $raw"; return 1; fi + local conflicts=$(nft -a list chain inet "${TABLE_NAME}" prerouting 2>/dev/null | grep "dport $lp" | grep -o 'handle [0-9]*' | awk '{print $2}') for ch in $conflicts; do msg "$(printf "${MSGS[overwrite]}" "$lp" "$ch")"; delete_by_handle "$ch"; done + local u_raw=$(cat /proc/sys/kernel/random/uuid); local u="ipf-id:$u_raw"; local fam="ip"; [[ "$tip" =~ : ]] && fam="ip6" + nft insert rule inet "${TABLE_NAME}" prerouting "$PROTO" dport "$lp" counter dnat $fam to "$tip:$tp" comment "\"$u\"" nft insert rule inet "${TABLE_NAME}" output "$PROTO" dport "$lp" counter dnat $fam to "$tip:$tp" comment "\"$u\"" nft insert rule inet "${TABLE_NAME}" forward $fam daddr "$tip" "$PROTO" dport "$tp" ct state new,established,related accept comment "\"$u\"" nft insert rule inet "${TABLE_NAME}" forward $fam saddr "$tip" "$PROTO" sport "$tp" ct state established,related accept comment "\"$u\"" nft insert rule inet "${TABLE_NAME}" forward iifname "lo" accept comment "\"$u\"" - nft insert rule inet "${TABLE_NAME}" postrouting $fam daddr "$tip" "$PROTO" dport "$tp" masquerade comment "\"$u\"" + + # ポストルートイング:SNAT モードか MASQUERADE モードかで分岐 + if [[ "$SNAT_FLAG" == true ]]; then + msg "$(printf "${MSGS[nat_mode]}")" + nft insert rule inet "${TABLE_NAME}" postrouting $fam daddr "$tip" "$PROTO" dport "$tp" \ + counter snat to type nat orig comment "\"$u\"" + else + msg "$(printf "${MSGS[masq_mode]}")" + nft insert rule inet "${TABLE_NAME}" postrouting $fam daddr "$tip" "$PROTO" dport "$tp" \ + masquerade comment "\"$u\"" + fi + list_rules "$u_raw" [[ "$SKIP_TEST" == false ]] && { local nh=$(nft -a list chain inet "${TABLE_NAME}" prerouting 2>/dev/null | grep "$u_raw" | grep -o 'handle [0-9]*' | awk '{print $2}'); echo ""; test_strict_handle "$nh"; } } @@ -288,6 +355,8 @@ for arg in "$@"; do case "$arg" in -q*) QUIET_MODE=true; FORCE_FLAG=true; SKIP_TEST=true ;; -f*|-y*) FORCE_FLAG=true; SKIP_TEST=true ;; + -F*) SNAT_FLAG=true; SKIP_TEST=true ;; + -u) PROTO="udp" ;; -R) RESET_MODE=true ;; esac done @@ -308,7 +377,9 @@ while [[ $# -gt 0 ]]; do -h|--help) show_help; exit 0 ;; -v|--version) echo "ipf version ${VERSION}"; exit 0 ;; -l|-L) list_rules; exit 0 ;; + -u) shift; [[ $# -eq 0 ]] && exit 0; continue ;; -f|-y|-q) [[ "$1" == "-f" ]] && enable_forwarding; shift; [[ $# -eq 0 ]] && exit 0; continue ;; + -F*) enable_forwarding; shift; [[ $# -eq 0 ]] && exit 0; continue ;; -*[d]*) target="${1#-d}"; [[ -z "$target" ]] && { target="$2"; shift; } [[ "$target" == "all" ]] && { nft delete table inet "${TABLE_NAME}" 2>/dev/null; init_nft; list_rules; exit 0; } @@ -322,13 +393,13 @@ while [[ $# -gt 0 ]]; do -t*) target="${1#-t}"; [[ -z "$target" ]] && { target="$2"; shift; } if [[ "$target" =~ ^([0-9\.]+):([0-9]+)$ ]]; then - echo -n "Target $target... "; test_connection "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" + echo -n "Target $target [$PROTO]... "; test_connection "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "$PROTO" elif [[ "$target" =~ ^:([0-9]+)$ ]]; then - echo -n "Local $target... "; test_connection "127.0.0.1" "${BASH_REMATCH[1]}" + echo -n "Local $target [$PROTO]... "; test_connection "127.0.0.1" "${BASH_REMATCH[1]}" "$PROTO" else test_strict_handle "$target"; fi exit 0 ;; [0-9]*) add_rule "$1"; exit 0 ;; *) err "$(printf "${MSGS[unknown_opt]}" "$1")"; exit 1 ;; esac shift -done +done \ No newline at end of file