Compare commits

...

3 commits

Author SHA1 Message Date
joe
6dd9812ec9 -t 拡張 ssh/http等を検出 2026-02-04 10:17:18 +09:00
joe
5f0c92953e -t 修正前 2026-02-04 09:55:44 +09:00
joe
69b25cdb59 1.6.3 1.6.1に回帰した系? 2026-01-23 11:10:40 +09:00
3 changed files with 493 additions and 157 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.cecli*

254
ipf
View file

@ -1,9 +1,9 @@
#!/bin/bash
# Name: ipf
# Version: 1.6.1
# Date: 2026-01-22
# Description: Fully-featured nftables wrapper.
# Refinement: Incorporated Qwen2.5-Coder's feedback with human-readable robustness.
# Version: 1.6.7
# Date: 2026-02-04
# Description: Finalized multi-protocol detection.
# Handles OpenResty/Nginx "Plain HTTP to HTTPS" (400) cases intelligently.
# -----------------------------------------------------------------------------
# 1. Root & Environment Check
@ -14,9 +14,9 @@ fi
# Global Configurations
TABLE_NAME="ipf"
VERSION="1.6.1"
VERSION="1.6.7"
PROTO="tcp"
FORCE_FLAG=false
FORCE_FLAG=false
SKIP_TEST=false
QUIET_MODE=false
RESET_MODE=false
@ -30,7 +30,6 @@ msg() {
}
err() {
# Even in quiet mode, critical errors should be visible but without color codes if requested
if [[ "$QUIET_MODE" == false ]]; then
echo -e "\e[31m$@\e[0m" >&2
else
@ -40,53 +39,77 @@ err() {
enable_forwarding() {
msg "\e[34m[System]\e[0m Enabling IP forwarding..."
# Qwen's point: Check sysctl success
if ! sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1; then
err "Critical: Could not enable net.ipv4.ip_forward. Please check your system permissions."
fi
# Enable route_localnet for all interfaces (essential for DNAT to 127.0.0.1)
sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1
for dev in /proc/sys/net/ipv4/conf/*/route_localnet; do
if [[ -f "$dev" ]]; then
echo 1 > "$dev" 2>/dev/null
fi
[[ -f "$dev" ]] && echo 1 > "$dev" 2>/dev/null
done
}
init_nft() {
# Create table if it doesn't exist
nft add table inet "${TABLE_NAME}" 2>/dev/null
# Qwen's point: Reliable chain initialization
# Prerouting (NAT)
nft add chain inet "${TABLE_NAME}" prerouting "{ type nat hook prerouting priority -100 ; policy accept ; }" 2>/dev/null
# Output (NAT for local traffic)
nft add chain inet "${TABLE_NAME}" output "{ type nat hook output priority -100 ; policy accept ; }" 2>/dev/null
# Postrouting (Masquerade)
nft add chain inet "${TABLE_NAME}" postrouting "{ type nat hook postrouting priority 100 ; policy accept ; }" 2>/dev/null
# Forward (Filter)
nft add chain inet "${TABLE_NAME}" forward "{ type filter hook forward priority 0 ; policy accept ; }" 2>/dev/null
}
# -----------------------------------------------------------------------------
# 3. Data & Packet Processing
# 3. Connectivity & Counter Functions
# -----------------------------------------------------------------------------
get_total_packets() {
local uuid=$1
# Optimized: One-pass sum of packets for all rules with this UUID
# We use awk to ensure we always return a number, even if no rules match.
local total=$(nft list table inet "${TABLE_NAME}" 2>/dev/null | grep "$uuid" | grep -o 'packets [0-9]*' | awk '{sum+=$2} END {print sum+0}')
echo "$total"
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 target_ip=$1
local target_port=$2
# Ensure standard NC argument order: [options] [host] [port]
nc -z -w 1 "$target_ip" "$target_port" >/dev/null 2>&1
return $?
local host=$1
local port=$2
local info=""
# A. Check for Protocol Banners (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. Check for HTTP/HTTPS
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
# 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
done
echo -e "\e[32mUP\e[0m${info}"
return 0
else
echo -e "\e[31mDOWN\e[0m"
return 1
fi
}
# -----------------------------------------------------------------------------
@ -96,19 +119,12 @@ test_connection() {
delete_by_handle() {
local h=$1
local uuid=""
# Qwen's point: Search UUID across ALL chains, not just prerouting
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)
[[ -n "$uuid" ]] && break
done
if [[ -z "$uuid" ]]; then
return 1
fi
# Delete all rules associated with this UUID in all relevant chains
[[ -z "$uuid" ]] && return 1
for c in "${search_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"
@ -120,12 +136,8 @@ 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")
if [[ -z "$info" ]]; then
err "Handle $h not found in table '${TABLE_NAME}'"
return
fi
[[ -z "$info" ]] && { err "Handle $h not found"; 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 '[]')
@ -135,19 +147,13 @@ test_strict_handle() {
echo -n "Testing handle $h (Local :$lp -> Target $tip:$tp) [${short_id}]... "
local count_before=$(get_total_packets "$uuid")
if test_connection "$tip" "$tp"; then
# Try to trigger the rule via localhost
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\e[0m"
else
echo -e "\e[33mSKIPPED (Traffic not hitting rule)\e[0m"
fi
else
echo -e "\e[31mOFFLINE (Target Unreachable)\e[0m"
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
}
@ -155,18 +161,15 @@ list_rules() {
[[ "$QUIET_MODE" == true ]] && return
local highlight_uuid=$1
init_nft
msg "Forwarding Rules (Table: ${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
@ -179,8 +182,6 @@ add_rule() {
local raw=$1
init_nft
[[ "$FORCE_FLAG" == true ]] && enable_forwarding
# Syntax Parsing (LPORT:TIP:TPORT / LPORT:TPORT / LPORT)
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
@ -188,138 +189,77 @@ add_rule() {
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
err "Error: Invalid rule format '$raw'"; return 1
fi
# --- Conflict Management ---
# Find and overwrite existing rules for the same local port
local existing_rules=$(nft -a list chain inet "${TABLE_NAME}" prerouting 2>/dev/null | grep "dport $lp" | grep -o 'handle [0-9]*' | awk '{print $2}')
for eh in $existing_rules; do
msg "\e[33mOverwriting existing rule on port :$lp (Handle $eh)...\e[0m"
delete_by_handle "$eh"
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
# Generate Unique ID for the rule set
local u_raw=$(cat /proc/sys/kernel/random/uuid)
local u="ipf-id:$u_raw"
local fam="ip"
[[ "$tip" =~ : ]] && fam="ip6"
# 1. PREROUTING: Redirect incoming external packets
local fam="ip"; [[ "$tip" =~ : ]] && fam="ip6"
nft insert rule inet "${TABLE_NAME}" prerouting "$PROTO" dport "$lp" counter dnat $fam to "$tip:$tp" comment "\"$u\""
# 2. OUTPUT: Redirect locally generated packets
nft insert rule inet "${TABLE_NAME}" output "$PROTO" dport "$lp" counter dnat $fam to "$tip:$tp" comment "\"$u\""
# 3. FORWARD: Allow traffic to the target
nft insert rule inet "${TABLE_NAME}" forward $fam daddr "$tip" "$PROTO" dport "$tp" ct state new,established,related accept comment "\"$u\""
# 4. FORWARD: Allow return traffic from the target
nft insert rule inet "${TABLE_NAME}" forward $fam saddr "$tip" "$PROTO" sport "$tp" ct state established,related accept comment "\"$u\""
# 5. FORWARD: Loopback interface allowance
nft insert rule inet "${TABLE_NAME}" forward iifname "lo" accept comment "\"$u\""
# 6. POSTROUTING: Source NAT to ensure target sees local IP
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"
echo ""; test_strict_handle "$nh"
fi
}
all_clear() {
if [[ "$FORCE_FLAG" == false ]]; then
echo -e "\e[33mWarning: This will delete ALL rules in table '${TABLE_NAME}'.\e[0m"
read -p "Confirm removal? (y/N): " confirm
[[ ! "$confirm" =~ ^[yY]$ ]] && exit 0
fi
nft delete table inet "${TABLE_NAME}" 2>/dev/null
init_nft
msg "Table '${TABLE_NAME}' has been reset."
list_rules
exit 0
}
# -----------------------------------------------------------------------------
# 5. Argument Parsing (Main Loop)
# 5. Main Parsing Loop
# -----------------------------------------------------------------------------
# Pre-parse flags for Global behavior
for arg in "$@"; do
if [[ "$arg" =~ q ]]; then QUIET_MODE=true; FORCE_FLAG=true; SKIP_TEST=true; fi
if [[ "$arg" =~ f ]]; then FORCE_FLAG=true; SKIP_TEST=true; fi
if [[ "$arg" =~ y ]]; then FORCE_FLAG=true; SKIP_TEST=true; fi
[[ "$arg" == "-R" ]] && RESET_MODE=true
case "$arg" in
-q*) QUIET_MODE=true; FORCE_FLAG=true; SKIP_TEST=true ;;
-f*|-y*) FORCE_FLAG=true; SKIP_TEST=true ;;
-R) RESET_MODE=true ;;
esac
done
if [[ "$RESET_MODE" == true ]]; then all_clear; fi
if [[ $# -eq 0 ]]; then list_rules; exit 0; fi
[[ "$RESET_MODE" == true ]] && { nft delete table inet "${TABLE_NAME}" 2>/dev/null; init_nft; msg "Reset done."; list_rules; exit 0; }
[[ $# -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:192.168.1.10:8080"
echo "Example: ipf 80:10.10.100.1:80"
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 ;;
-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 ;;
-*[d]*)
# Support combined flags and comma-separated handles
target="${1#-d}"
[[ -z "$target" ]] && { target="$2"; shift; }
[[ "$target" == "all" ]] && all_clear
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
# Delete by local port
port="${BASH_REMATCH[1]}"
for h in $(nft -a list chain inet "${TABLE_NAME}" prerouting 2>/dev/null | grep "dport $port" | grep -o 'handle [0-9]*' | awk '{print $2}'); do
delete_by_handle "$h"
done
else
# Delete by handle
if ! delete_by_handle "$p"; then
err "Error: Handle $p not found."
fi
fi
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
else delete_by_handle "$p"; fi
done
list_rules; exit 0 ;;
-t*)
# Support combined or separated test target
target="${1#-t}"
[[ -z "$target" ]] && { target="$2"; shift; }
target="${1#-t}"; [[ -z "$target" ]] && { target="$2"; shift; }
if [[ "$target" =~ ^([0-9\.]+):([0-9]+)$ ]]; then
# External Target IP:PORT test
ip=${BASH_REMATCH[1]}; port=${BASH_REMATCH[2]}
echo -n "Checking Target $ip:$port... "
test_connection "$ip" "$port" && echo -e "\e[32mUP\e[0m" || echo -e "\e[31mDOWN\e[0m"
echo -n "Target $target... "; test_connection "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}"
elif [[ "$target" =~ ^:([0-9]+)$ ]]; then
# Local port test
port="${BASH_REMATCH[1]}"
echo -n "Checking Local :$port... "
nc -z -w 1 127.0.0.1 "$port" >/dev/null 2>&1 && echo -e "\e[32mOK\e[0m" || echo -e "\e[31mOFFLINE\e[0m"
elif [[ "$target" =~ ^[0-9]+$ ]]; then
# Internal rule test by handle
test_strict_handle "$target"
echo -n "Local $target... "; test_connection "127.0.0.1" "${BASH_REMATCH[1]}"
else
# Default to testing first available rule
top_h=$(nft -a list chain inet "${TABLE_NAME}" prerouting 2>/dev/null | grep "dnat" | head -n 1 | grep -o 'handle [0-9]*' | awk '{print $2}')
[[ -n "$top_h" ]] && test_strict_handle "$top_h" || err "Error: No valid target to test."
test_strict_handle "$target"
fi
exit 0 ;;
[0-9]*)
add_rule "$1"; exit 0 ;;
*)
err "Unknown option '$1'. Use --help for usage."; exit 1 ;;
[0-9]*) add_rule "$1"; exit 0 ;;
*) err "Unknown option '$1'."; exit 1 ;;
esac
shift
done

395
ipf1.0.4 Executable file
View file

@ -0,0 +1,395 @@
#!/bin/bash
# sudo権限で実行されているか確認し、そうでなければsudoで再実行
if [ "$(id -u)" -ne 0 ]; then
exec sudo "$0" "$@"
fi
# グローバル変数
RULES_FILE="/tmp/iptables_forward_rules"
QUIET=false
PROTO="tcp" # デフォルトプロトコル
# コンテナ環境検出関数
is_container() {
# systemd-detect-virt を使用してコンテナ環境を検出
if command -v systemd-detect-virt &> /dev/null; then
if systemd-detect-virt --quiet | grep -qE "container|vm"; then
return 0 # コンテナまたは仮想マシン環境
fi
fi
# lxc-checkconfig を使用して LXC コンテナを検出 (systemd-detect-virt がない場合)
if command -v lxc-checkconfig &> /dev/null; then
if lxc-checkconfig 2>&1 | grep -q "Running in an LXC container"; then
return 0 # LXC コンテナ環境
fi
fi
# その他 (LXD など) のコンテナ環境検出方法を追加可能
return 1 # コンテナ環境ではない
}
# ヘルプ表示
show_help() {
cat << EOF
Usage: ipf [OPTIONS] [PORT:IP:PORT | -L | -d RULE_NUMBER]
Examples:
ipf 11434:10.1.1.2:11434 # Forward local port 11434 to 10.1.1.2:11434
ipf -L # List all rules with numbers
ipf -d 1 # Delete rule number 1
ipf -d 1 -q # Delete rule number 1 (quiet mode)
ipf -v # Show current iptables rules by iptables-save
ipf -e # Edit all rules in editor and restore
ipf -f # Enable IP forwarding
ipf -t 1 # Test rule number 1
ipf -t 11434:10.10.1.2:11434 # Test rule by specification
ipf -p udp 11434:10.1.1.2:11434 # Forward using UDP
Options:
-h, --help Show this help message
-L, -l List all rules
-d NUM Delete rule by number
-q Quiet mode (no output)
-v Show current iptables rules (via iptables-save)
-e Edit all rules in editor and restore
-f Enable IP forwarding and localnet routing
-t ARG Test connectivity for rule number or PORT:IP:PORT
-p PROTO Specify protocol (tcp|udp) for the following rule
--version Show version information
NOTE:
All port forwarding is performed using DNAT only. No MASQUERADE (SNAT) is applied,
so the original source IP of the client is preserved. This ensures that
fail2ban logs the correct client IP on the destination server.
EOF
}
# バージョン情報を表示
show_version() {
cat << EOF
ipf ver.1.0.4
Date: 2025-10-26
Created by: qwen3/gpt-oss/gemini-2.5-pro and krasherjoe
---
MIT License
Copyright (c) 2025 krasherjoe
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
** WARNING / 注意 **
This script operates with administrative privileges (sudo) to modify your system's
firewall (iptables) rules. Incorrect use, especially with the -e (edit) option,
can disrupt your network connectivity or overwrite existing security rules.
Please use with caution and understand the changes you are making.
このスクリプトは管理者権限(sudo)で動作し、システムのファイアウォール(iptables)ルールを
直接変更します。特に -e (編集) オプションの誤った使用は、ネットワーク接続を中断させたり、
既存のセキュリティルールを上書きする危険性があります。
コンテナ環境ではMASQUERADEを使用するため、クライアントの元のIPアドレスが隠蔽されます。
これにより、fail2banなどのツールが不正な行為者を正しく識別し、ブロックできなくなる可能性があります。
セキュリティへの影響を考慮してください。
内容をよく理解した上で、注意して使用してください。
EOF
}
# エラーメッセージを表示
error() {
echo "Error: $*" >&2
logger -p user.err "ipf: $*"
exit 1
}
# ルールの保存
save_rules() {
iptables-save > "$RULES_FILE"
}
# ルールの読み込み
load_rules() {
if [[ -f "$RULES_FILE" ]]; then
iptables-restore < "$RULES_FILE"
fi
}
# ルール番号と内容をリスト表示
list_rules() {
local i=1
echo "Forwarding rules:"
iptables -t nat -L PREROUTING -n -v --line-numbers | grep -E '^[0-9]+' | while read -r line; do
echo "$i: $line"
((i++))
done
}
# ルールを削除
delete_rule() {
local num=$1
if ! [[ "$num" =~ ^[0-9]+$ ]]; then
error "Invalid rule number: $num"
fi
# ユーザーへの表示用に整形されたルール行を取得
local rule_line_verbose=$(iptables -t nat -L PREROUTING --line-numbers | grep -E "^$num\s+" | head -n 1)
if [[ -z "$rule_line_verbose" ]]; then
error "No rule found with number: $num"
fi
# パース用に-nオプションを付けたルール行を取得
local rule_line_numeric=$(iptables -t nat -L PREROUTING -n --line-numbers | grep -E "^\s*$num\s+" | head -n 1)
if [[ -z "$rule_line_numeric" ]]; then
error "Could not find numeric rule for number: $num" # Should not happen
fi
# ルールから詳細を抽出 (sedを使い、より堅牢に)
local line_details=$(echo "$rule_line_numeric" | sed -n 's/.*\(tcp\|udp\).*dpt:\([0-9]*\).*to:\([0-9.]*\):\([0-9]*\).*/\1 \2 \3 \4/p')
if [[ -z "$line_details" ]]; then
error "Could not parse rule details from line: $rule_line_numeric"
fi
read -r proto local_port target_ip target_port <<< "$line_details"
# 1. PREROUTING ルールを番号で削除
if ! iptables -t nat -D PREROUTING "$num"; then
error "Failed to delete PREROUTING rule $num"
fi
# 2. 対応する OUTPUT ルールをすべて削除
while iptables -t nat -D OUTPUT -p "$proto" -m "$proto" --dport "$local_port" -j DNAT --to-destination "$target_ip:$target_port" >/dev/null 2>&1; do :; done
# 3. 対応する FORWARD ルールをすべて削除
while iptables -D FORWARD -p "$proto" -m "$proto" -d "$target_ip" --dport "$target_port" -j ACCEPT >/dev/null 2>&1; do :; done
# 4. 確立済み通信を許可するルールを削除 (存在確認後に削除)
if iptables -C FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null; then
iptables -D FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT >/dev/null 2>&1
fi
if ! $QUIET; then
echo "Deleted rule $num: $rule_line_verbose"
fi
}
# ポートフォワーディングルールを追加
add_rule() {
# IPフォワーディングが有効かチェック
if [[ $(sysctl -n net.ipv4.ip_forward) -ne 1 ]]; then
error "IP forwarding is disabled. Please enable it by running: ipf -f"
fi
local container_env=$(is_container)
if [[ $container_env -eq 0 ]]; then
echo -e "\e[33mDetected container environment. MASQUERADE is enabled, which hides the original client IP address. This will prevent tools like fail2ban from correctly identifying and blocking malicious actors. Consider the security implications.\e[0m" # コンテナ環境であることを示すメッセージ
fi
# 確立済みの通信は許可する (戻りのパケットのため)
iptables -I FORWARD 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
local port_ip_port=$1
local local_port=$(echo "$port_ip_port" | cut -d':' -f1)
local target_ip=$(echo "$port_ip_port" | cut -d':' -f2)
local target_port=$(echo "$port_ip_port" | cut -d':' -f3)
# IPアドレス検証
if ! [[ "$target_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
error "Invalid IP address: $target_ip"
fi
# ポート番号検証
if ! [[ "$local_port" =~ ^[0-9]+$ ]] || ! [[ "$target_port" =~ ^[0-9]+$ ]]; then
error "Invalid port number"
fi
if (( local_port < 1 || local_port > 65535 || target_port < 1 || target_port > 65535 )); then
error "Port number out of range (1-65535)"
fi
# iptablesでルールを追加
# 外部からのパケットを対象
iptables -t nat -A PREROUTING -p "$PROTO" -m "$PROTO" --dport "$local_port" -j DNAT --to-destination "$target_ip:$target_port"
# ローカルで生成されたパケットを対象
iptables -t nat -A OUTPUT -p "$PROTO" -m "$PROTO" --dport "$local_port" -j DNAT --to-destination "$target_ip:$target_port"
if [[ $container_env -eq 0 ]]; then
# コンテナ環境ではMASQUERADEを使用
iptables -t nat -A POSTROUTING -p "$PROTO" -m "$PROTO" -d "$target_ip" --dport "$target_port" -j MASQUERADE
fi
# 転送されるパケットを許可する
iptables -A FORWARD -p "$PROTO" -m "$PROTO" -d "$target_ip" --dport "$target_port" -j ACCEPT
if ! $QUIET; then
echo "Rule added: port $local_port -> $target_ip:$target_port (proto=$PROTO)"
fi
}
# ルールを編集して復元
edit_rules() {
local temp_file
temp_file=$(mktemp -t iptables.rules.XXXXXX) || error "Failed to create temporary file"
chmod 0600 "$temp_file"
trap 'rm -f "$temp_file"' EXIT
# 現在のルールを一時ファイルに保存
iptables-save > "$temp_file"
# エディタを決定
local editor=${EDITOR:-nano}
if ! command -v "$editor" > /dev/null; then
editor=vi
fi
# nanoで編集
if ! "$editor" "$temp_file"; then
error "Editor closed without saving or an error occurred."
fi
# 編集後の内容でリストア
if iptables-restore < "$temp_file"; then
echo "iptables rules restored successfully from your edits."
echo "--- Displaying new rules ---"
iptables-save
else
error "Failed to restore iptables rules. Please check for syntax errors in your edits."
fi
}
# 疎通確認用関数
test_rule() {
local arg=$1
local local_port target_ip target_port
if [[ "$arg" =~ ^[0-9]+$ ]]; then
# rule number → ルール行を取得
local line=$(iptables -t nat -L PREROUTING -n --line-numbers | grep "^$arg ")
if [[ -z "$line" ]]; then
error "No rule found with number: $arg"
fi
local_port=$(echo "$line" | awk '{print $8}'|cut -d ':' -f2)
target_ip=$(echo "$line" | awk -F: '{print $1}')
target_port=$(echo "$line" | awk -F: '{print $2}')
else
# 文字列形式
local_port=$(echo "$arg" | cut -d':' -f1)
target_ip=$(echo "$arg" | cut -d':' -f2)
target_port=$(echo "$arg" | cut -d':' -f3)
fi
echo "Testing connectivity to local port \"$(tput setaf 3)nc -z -w 5 127.0.0.1 $local_port$(tput sgr0)\""
if nc -z -w 5 127.0.0.1 "$local_port" 2>/dev/null; then
echo "$(tput setaf 6)Connection successful.$(tput sgr0)"
else
echo "$(tput setaf 1)Connection failed.$(tput sgr0)"
fi
}
# メイン処理
main() {
# オプション解析
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
show_help
exit 0
;;
-L|-l)
list_rules
exit 0
;;
-v)
iptables-save | grep -v '^#' | less -R
exit 0
;;
-e)
edit_rules
exit 0
;;
-f)
if sysctl -w net.ipv4.ip_forward=1 > /dev/null && \
sysctl -w net.ipv4.conf.all.route_localnet=1 > /dev/null && \
sysctl -w net.ipv4.conf.default.route_localnet=1 > /dev/null; then
echo "IP forwarding and localnet routing enabled."
else
error "Failed to enable kernel parameters."
fi
exit 0
;;
--version)
show_version
exit 0
;;
-d)
if [[ $# -lt 2 ]]; then
error "Missing rule number for -d option"
fi
delete_rule "$2"
exit 0
;;
-t)
if [[ $# -lt 2 ]]; then
error "Missing argument for -t option"
fi
test_rule "$2"
exit 0
;;
-q)
QUIET=true
shift
;;
-p)
if [[ $# -lt 2 ]]; then
error "Missing protocol after -p"
fi
if [[ "$2" != "tcp" && "$2" != "udp" ]]; then
error "Unsupported protocol: $2"
fi
PROTO="$2"
shift 2
;;
-*)
error "Unknown option: $1"
;;
*)
break
;;
esac
done
# 引数が残っていれば、ルール追加処理
if [[ $# -eq 0 ]]; then
show_help
exit 1
fi
local arg=$1
# ルール追加処理
if [[ "$arg" =~ ^[0-9]+:[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+$ ]]; then
add_rule "$arg"
else
error "Invalid format. Expected: PORT:IP:PORT or -L or -d RULE_NUMBER"
fi
}
# 実行
main "$@"