From d11c152767b7ba9def9b18ac2672bba8156813a9 Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 4 Feb 2026 10:24:13 +0900 Subject: [PATCH] =?UTF-8?q?=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=81=AB=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ipf | 266 ++++++++++++++++++++++++++++++++++-------------------------- 1 file changed, 152 insertions(+), 114 deletions(-) diff --git a/ipf b/ipf index eba0166..b293b8c 100755 --- a/ipf +++ b/ipf @@ -1,20 +1,59 @@ #!/bin/bash # Name: ipf -# Version: 1.6.7 +# Version: 1.6.9 # Date: 2026-02-04 -# Description: Finalized multi-protocol detection. -# Handles OpenResty/Nginx "Plain HTTP to HTTPS" (400) cases intelligently. +# Description: Fully featured port forwarder with rich help, multi-protocol +# detection (SSH/HTTP/HTTPS/etc.), and JP/EN localization. # ----------------------------------------------------------------------------- -# 1. Root & Environment Check +# 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 でヘルプを表示してください。" + ) +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." + ) +fi + +# ----------------------------------------------------------------------------- +# 2. Root Check & Init # ----------------------------------------------------------------------------- if [ "$(id -u)" -ne 0 ]; then exec sudo "$0" "$@" fi -# Global Configurations TABLE_NAME="ipf" -VERSION="1.6.7" +VERSION="1.6.9" PROTO="tcp" FORCE_FLAG=false SKIP_TEST=false @@ -22,23 +61,14 @@ QUIET_MODE=false RESET_MODE=false # ----------------------------------------------------------------------------- -# 2. Messaging & System Functions +# 3. Core Functions # ----------------------------------------------------------------------------- -msg() { - [[ "$QUIET_MODE" == false ]] && echo -e "$@" -} - -err() { - if [[ "$QUIET_MODE" == false ]]; then - echo -e "\e[31m$@\e[0m" >&2 - else - echo "$@" >&2 - fi -} +msg() { [[ "$QUIET_MODE" == false ]] && echo -e "$@"; } +err() { [[ "$QUIET_MODE" == false ]] && echo -e "\e[31m$@\e[0m" >&2 || echo "$@" >&2; } enable_forwarding() { - msg "\e[34m[System]\e[0m Enabling IP 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 @@ -53,21 +83,9 @@ init_nft() { nft add chain inet "${TABLE_NAME}" forward "{ type filter hook forward priority 0 ; policy accept ; }" 2>/dev/null } -# ----------------------------------------------------------------------------- -# 3. Connectivity & Counter Functions -# ----------------------------------------------------------------------------- - -get_total_packets() { - local uuid=$1 - 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() { - local host=$1 - local port=$2 - local info="" - - # A. Check for Protocol Banners (SSH, FTP, etc.) + local host=$1; local port=$2; local info="" + # Protocol Banner Check 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 @@ -77,55 +95,94 @@ test_connection() { "mysql"*) info=" (MySQL)" ;; *) info=" (Banner: ${banner:0:15})" ;; esac - echo -e "\e[32mUP\e[0m${info}" - return 0 + echo -e "\e[32mUP\e[0m${info}"; return 0 fi - - # B. Check for HTTP/HTTPS + # HTTP/HTTPS Check if nc -z -w 1 "$host" "$port" >/dev/null 2>&1; then - local schemas=("http" "https") - [[ "$port" == "443" ]] && schemas=("https" "http") - + local schemas=("http" "https"); [[ "$port" == "443" ]] && schemas=("https" "http") for sch in "${schemas[@]}"; do - # Try HEAD first local res=$(curl -IsLk -m 2 -A "Mozilla/5.0 ipf-tester" -o /dev/null -w "%{http_code}" "${sch}://${host}:${port}/" 2>/dev/null) - - # Try GET if HEAD fails (timeout or 000) - if [[ "$res" == "000" ]]; then - res=$(curl -sLk -m 2 -A "Mozilla/5.0 ipf-tester" -o /dev/null -w "%{http_code}" "${sch}://${host}:${port}/" 2>/dev/null) - fi - - if [[ "$res" == "400" ]]; then - # Special Case: Nginx/OpenResty plain HTTP to HTTPS port - info=" (HTTPS? 400 Bad Request)" - break - elif [[ "$res" =~ ^[0-9]+$ && "$res" -ne 000 ]]; then - info=" (${sch^^} $res)" - break - fi + [[ "$res" == "000" ]] && res=$(curl -sLk -m 2 -A "Mozilla/5.0 ipf-tester" -o /dev/null -w "%{http_code}" "${sch}://${host}:${port}/" 2>/dev/null) + if [[ "$res" == "400" ]]; then info=" (HTTPS? 400 Bad Request)"; break + elif [[ "$res" =~ ^[0-9]+$ && "$res" -ne 000 ]]; then info=" (${sch^^} $res)"; break; fi done - echo -e "\e[32mUP\e[0m${info}" - return 0 + echo -e "\e[32mUP\e[0m${info}"; return 0 else - echo -e "\e[31mDOWN\e[0m" - return 1 + 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または :ポート でルールを削除 + 例: ipf -d 12 / ipf -d :80 + -d all すべてのルールを削除してテーブルを初期化 + -t 指定したターゲットの接続性とプロトコルをテスト + -t 既存ルールのハンドルIDを指定して疎通テストを実行 + -R 設定リセット(テーブルを再作成) + -f, -y 確認なしで実行し、IPフォワーディングを強制有効化 + -q クワイエットモード(メッセージ出力を抑制) + -h, --help この詳細ヘルプを表示 +EOF + else + cat << EOF +ipf (IP Forwarder) - Simple nftables-based port forwarding manager + +Usage: + ipf [OPTIONS] [RULE] + +Rule Formats: + :: + Ex: ipf 8080:10.10.100.1:80 + + : + Ex: ipf 80:8080 + + + Ex: ipf 22 + +Options: + -l, -L List current rules + -d Delete rule by handle or :port + -d all Flush all rules + -t Test connectivity/protocol + -R Reset table + -h, --help Show this help +EOF fi } # ----------------------------------------------------------------------------- -# 4. Rule Management Functions +# 4. Implementation Logic (List, Add, Delete) # ----------------------------------------------------------------------------- delete_by_handle() { - local h=$1 - local uuid="" - local search_chains=("prerouting" "output" "forward" "postrouting") - for sc in "${search_chains[@]}"; do - uuid=$(nft -a list chain inet "${TABLE_NAME}" "$sc" 2>/dev/null | grep "handle $h" | grep -o 'ipf-id:[a-z0-9-]*' | head -n 1) + 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 "${search_chains[@]}"; do + 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 @@ -136,32 +193,26 @@ delete_by_handle() { 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 "Handle $h not found"; return; } - + [[ -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) - - echo -n "Testing handle $h (Local :$lp -> Target $tip:$tp) [${short_id}]... " - local count_before=$(get_total_packets "$uuid") + 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 - local count_after=$(get_total_packets "$uuid") - if (( count_after > count_before )); then - echo -e " \e[32mPASSED (Counter: +$((count_after - count_before)))\e[0m" - fi + nc -z -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 "Forwarding Rules (Table: ${TABLE_NAME}):" + 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 @@ -172,15 +223,12 @@ list_rules() { 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 + 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 + 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]} @@ -188,17 +236,10 @@ add_rule() { 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 "Error: Invalid rule format '$raw'"; return 1 - fi + 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 "\e[33mOverwriting existing rule on port :$lp (Handle $ch)...\e[0m" - 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" + 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\"" @@ -206,16 +247,12 @@ add_rule() { 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\"" list_rules "$u_raw" - if [[ "$SKIP_TEST" == false && "$FORCE_FLAG" == false ]]; then - 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" - fi + [[ "$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 Parsing Loop +# 5. Main Loop # ----------------------------------------------------------------------------- - for arg in "$@"; do case "$arg" in -q*) QUIET_MODE=true; FORCE_FLAG=true; SKIP_TEST=true ;; @@ -224,16 +261,20 @@ for arg in "$@"; do esac done -[[ "$RESET_MODE" == true ]] && { nft delete table inet "${TABLE_NAME}" 2>/dev/null; init_nft; msg "Reset done."; list_rules; exit 0; } +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) - echo "ipf version ${VERSION}" - echo "Usage: ipf [OPTIONS] [RULE]" - echo "Example: ipf 80:10.10.100.1:80" - exit 0 ;; + -h|--help) show_help; exit 0 ;; -v|--version) echo "ipf version ${VERSION}"; exit 0 ;; -l|-L) list_rules; exit 0 ;; -f|-y|-q) [[ "$1" == "-f" ]] && enable_forwarding; shift; [[ $# -eq 0 ]] && exit 0; continue ;; @@ -243,8 +284,7 @@ while [[ $# -gt 0 ]]; do IFS=',' read -r -a parts <<< "$target" for p in "${parts[@]}"; do if [[ "$p" =~ ^:([0-9]+)$ ]]; then - ports=$(nft -a list chain inet "${TABLE_NAME}" prerouting 2>/dev/null | grep "dport ${BASH_REMATCH[1]}" | grep -o 'handle [0-9]*' | awk '{print $2}') - for h in $ports; do delete_by_handle "$h"; done + 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 ;; @@ -254,12 +294,10 @@ while [[ $# -gt 0 ]]; do echo -n "Target $target... "; test_connection "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" elif [[ "$target" =~ ^:([0-9]+)$ ]]; then echo -n "Local $target... "; test_connection "127.0.0.1" "${BASH_REMATCH[1]}" - else - test_strict_handle "$target" - fi + else test_strict_handle "$target"; fi exit 0 ;; [0-9]*) add_rule "$1"; exit 0 ;; - *) err "Unknown option '$1'."; exit 1 ;; + *) err "$(printf "${MSGS[unknown_opt]}" "$1")"; exit 1 ;; esac shift done