ipf/ipf
2026-03-05 21:37:53 +09:00

405 lines
No EOL
18 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# Name: ipf
# 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.
# -----------------------------------------------------------------------------
# 1. Locale & Messaging Setup
# -----------------------------------------------------------------------------
SYSTEM_LANG=$([[ "$LANG" =~ ^ja ]] && echo "ja" || echo "en")
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):"
[overwrite]="\e[33mポート :%s (ハンドル %s) の既存ルールを上書きします...\e[0m"
[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"
[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=(
[system]="\e[34m[System]\e[0m"
[enabling_fwd]="Enabling IP forwarding..."
[err_fwd]="Critical: Could not enable net.ipv4.ip_forward."
[rules_header]="Forwarding Rules (Table: %s):"
[overwrite]="\e[33mOverwriting existing rule on port :%s (Handle %s)...\e[0m"
[reset_warn]="\e[33mWarning: This will delete ALL rules in table '%s'.\e[0m"
[confirm]="Confirm removal? (y/N): "
[reset_done]="Table '%s' has been reset."
[testing]="Testing handle %s (Local :%s -> Target %s:%s) [%s]... "
[passed]=" \e[32mPASSED (Counter: +%s)\e[0m"
[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
# -----------------------------------------------------------------------------
# 2. Root Check & Init
# -----------------------------------------------------------------------------
if [ "$(id -u)" -ne 0 ]; then
exec sudo "$0" "$@"
fi
TABLE_NAME="ipf"
VERSION="1.7.0"
PROTO="tcp"
FORCE_FLAG=false
SKIP_TEST=false
QUIET_MODE=false
RESET_MODE=false
SNAT_FLAG=false
# -----------------------------------------------------------------------------
# 3. Core Functions
# -----------------------------------------------------------------------------
msg() { [[ "$QUIET_MODE" == false ]] && echo -e "$@"; }
err() { [[ "$QUIET_MODE" == false ]] && echo -e "\e[31m$@\e[0m" >&2 || echo "$@" >&2; }
enable_forwarding() {
msg "${MSGS[system]} ${MSGS[enabling_fwd]}"
sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1
for dev in /proc/sys/net/ipv4/conf/*/route_localnet; do
[[ -f "$dev" ]] && echo 1 > "$dev" 2>/dev/null
done
}
init_nft() {
nft add table inet "${TABLE_NAME}" 2>/dev/null
nft add chain inet "${TABLE_NAME}" prerouting "{ type nat hook prerouting priority -100 ; policy accept ; }" 2>/dev/null
nft add chain inet "${TABLE_NAME}" output "{ type nat hook output priority -100 ; policy accept ; }" 2>/dev/null
nft add chain inet "${TABLE_NAME}" postrouting "{ type nat hook postrouting priority 100 ; policy accept ; }" 2>/dev/null
nft add chain inet "${TABLE_NAME}" forward "{ type filter hook forward priority 0 ; policy accept ; }" 2>/dev/null
}
test_connection() {
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')
if [[ -n "$banner" ]]; then
case "$banner" in
SSH*) info=" (SSH: ${banner:0:15})" ;;
"220"*) info=" (FTP/SMTP)" ;;
"RFB"*) info=" (VNC)" ;;
"mysql"*) info=" (MySQL)" ;;
*) info=" (Banner: ${banner:0:15})" ;;
esac
echo -e "\e[32mUP\e[0m${info}"; return 0
fi
# B. HTTP/HTTPS & Ollama Check
if nc -z -w 1 "$host" "$port" >/dev/null 2>&1; then
local schemas=("http" "https")
[[ "$port" == "443" ]] && schemas=("https" "http")
for sch in "${schemas[@]}"; do
# Ollama Check (Port 11434 or general HTTP)
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" が含まれているか)
if [[ "$res" == *"models"* ]]; then
info=" (Ollama API)"
echo -e "\e[32mUP\e[0m${info}"
# "__" を含むモデル名の抽出
local hidden_models=$(echo "$res" | grep -oP '"name":"[^"]*__[^"]*"' | cut -d'"' -f4)
if [[ -n "$hidden_models" ]]; then
echo -e " \e[35m[!] Hidden Models Found:\e[0m"
while read -r m; do
echo -e " - $m"
done <<< "$hidden_models"
fi
return 0
fi
# 通常の 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)
if [[ "$code" == "400" ]]; then
info=" (HTTPS? 400 Bad Request)"; break
elif [[ "$code" =~ ^[0-9]+$ && "$code" -ne 000 ]]; then
info=" (${sch^^} $code)"; break
fi
done
echo -e "\e[32mUP\e[0m${info}"
return 0
else
echo -e "\e[31mDOWN\e[0m"; return 1
fi
}
show_help() {
if [[ "$SYSTEM_LANG" == "ja" ]]; then
cat << EOF
ipf (IP Forwarder) - nftables ベースのポート転送管理ツール
使用法:
ipf [オプション] [ルール]
ルール形式:
<ローカルポート>:<ターゲット IP>:<ターゲットポート>
ipf 8080:10.10.100.1:80 (外部アクセス 8080 を内部 80 へ)
<ローカルポート>:<ターゲットポート>
ipf 80:8080 (外部 80 をローカル 8080 へ)
<ポート番号>
ipf 22 (外部 22 をローカル 22 へ)
オプション:
-l, -L 現在の転送ルールを一覧表示 (引数なしのデフォルト)
-d <ID/PORT> 指定したハンドル ID または : ポート でルールを削除
ipf -d 12 / ipf -d :80
-d all すべてのルールを削除してテーブルを初期化
-t <IP:PORT> 指定したターゲットの接続性とプロトコルをテスト
-t <HANDLE> 既存ルールのハンドル 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
ipf (IP Forwarder) - Simple nftables-based port forwarding manager
Usage:
ipf [OPTIONS] [RULE]
Rule Formats:
<LPORT>:<TIP>:<TPORT>
Ex: ipf 8080:10.10.100.1:80
<LPORT>:<TPORT>
Ex: ipf 80:8080
<PORT>
Ex: ipf 22
Options:
-l, -L List current rules
-d <ID/PORT> Delete rule by handle or :port
-d all Flush all rules
-t <IP:PORT> 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
-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
}
# -----------------------------------------------------------------------------
# 4. Implementation Logic (List, Add, Delete)
# -----------------------------------------------------------------------------
delete_by_handle() {
local h=$1; local uuid=""
local chains=("prerouting" "output" "forward" "postrouting")
for c in "${chains[@]}"; do
uuid=$(nft -a list chain inet "${TABLE_NAME}" "$c" 2>/dev/null | grep "handle $h" | grep -o 'ipf-id:[a-z0-9-]*' | head -n 1)
[[ -n "$uuid" ]] && break
done
[[ -z "$uuid" ]] && return 1
for c in "${chains[@]}"; do
nft -a list chain inet "${TABLE_NAME}" "$c" 2>/dev/null | grep "$uuid" | grep -o 'handle [0-9]*' | awk '{print $2}' | while read -r rh; do
nft delete rule inet "${TABLE_NAME}" "$c" handle "$rh"
done
done
return 0
}
test_strict_handle() {
local h=$1
local info=$(nft -a list chain inet "${TABLE_NAME}" prerouting 2>/dev/null | grep "handle $h")
[[ -z "$info" ]] && { err "$(printf "${MSGS[err_not_found]}" "$h")"; return; }
local lp=$(echo "$info" | grep -o 'dport [0-9]*' | awk '{print $2}')
local target=$(echo "$info" | grep -oE 'to ([0-9.]+|\[[0-9a-fA-F:]+\]):[0-9]+' | awk '{print $2}')
local tip=$(echo "$target" | cut -d':' -f1 | tr -d '[]')
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" "$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
}
list_rules() {
[[ "$QUIET_MODE" == true ]] && return
local highlight_uuid=$1; init_nft
msg "$(printf "${MSGS[rules_header]}" "${TABLE_NAME}")"
printf "%-10s %-6s %-20s %-20s %-10s\n" "HANDLE" "PROTO" "LOCAL" "TARGET" "UUID"
echo "-----------------------------------------------------------------------------------"
nft -a list chain inet "${TABLE_NAME}" prerouting 2>/dev/null | grep "dnat" | while read -r line; do
local handle=$(echo "$line" | grep -o 'handle [0-9]*' | awk '{print $2}')
local proto=$(echo "$line" | grep -q "udp" && echo "udp" || echo "tcp")
local target=$(echo "$line" | grep -oE '([0-9.]+|\[[0-9a-fA-F:]+\]):[0-9]+' | head -n 1)
local lport=$(echo "$line" | grep -o 'dport [0-9]*' | awk '{print $2}')
local full_uuid=$(echo "$line" | grep -o 'ipf-id:[a-z0-9-]*' | cut -d':' -f2)
if [[ -n "$highlight_uuid" && "$full_uuid" == "$highlight_uuid" ]]; then
printf "\e[1;36m*%-9s %-6s %-20s %-20s %-10s\e[0m\n" "$handle" "$proto" ":$lport" "$target" "${full_uuid:0:8}"
else printf " %-9s %-6s %-20s %-20s %-10s\n" "$handle" "$proto" ":$lport" "$target" "${full_uuid:0:8}"; fi
done
}
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
lp=${BASH_REMATCH[1]}; tip="127.0.0.1"; tp=${BASH_REMATCH[2]}
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\""
# ポストルートイング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"; }
}
# -----------------------------------------------------------------------------
# 5. Main Loop
# -----------------------------------------------------------------------------
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
if [[ "$RESET_MODE" == true ]]; then
if [[ "$FORCE_FLAG" == false ]]; then
msg "$(printf "${MSGS[reset_warn]}" "${TABLE_NAME}")"
read -p "${MSGS[confirm]}" confirm
[[ ! "$confirm" =~ ^[yY]$ ]] && exit 0
fi
nft delete table inet "${TABLE_NAME}" 2>/dev/null; init_nft; msg "$(printf "${MSGS[reset_done]}" "${TABLE_NAME}")"; list_rules; exit 0
fi
[[ $# -eq 0 ]] && { list_rules; exit 0; }
while [[ $# -gt 0 ]]; do
case "$1" in
-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; }
IFS=',' read -r -a parts <<< "$target"
for p in "${parts[@]}"; do
if [[ "$p" =~ ^:([0-9]+)$ ]]; then
for h in $(nft -a list chain inet "${TABLE_NAME}" prerouting 2>/dev/null | grep "dport ${BASH_REMATCH[1]}" | grep -o 'handle [0-9]*' | awk '{print $2}'); do delete_by_handle "$h"; done
else delete_by_handle "$p"; fi
done
list_rules; exit 0 ;;
-t*)
target="${1#-t}"; [[ -z "$target" ]] && { target="$2"; shift; }
if [[ "$target" =~ ^([0-9\.]+):([0-9]+)$ ]]; then
echo -n "Target $target [$PROTO]... "; test_connection "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "$PROTO"
elif [[ "$target" =~ ^:([0-9]+)$ ]]; then
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