ipf/ipfn1.0
joe d61b305520 ipfn1.0
ご提示いただいた `iptables` ベースのポートフォワーディング管理スクリプト (`ipf1.0.4`) を、**`nftables` ベースに完全リファクタリング**しました。

### 主な変更点と設計思想

1. **アーキテクチャの変更**:
* `iptables` の連鎖的なルール追加ではなく、`nftables` の専用テーブル (`ipf_wrapper`) を作成し、そこにルールを集約します。これにより、既存のシステム設定(FirewalldやUFWなど)との競合を最小限に抑えます。


2. **管理ID (UUID) の導入**:
* `iptables` ではルール番号(行番号)で管理していましたが、`nftables` は「ハンドル」を使用します。
* 1つのポート転送設定で複数のチェーン(PREROUTING, FORWARD, OUTPUTなど)にルールが分散するため、これらを **一意なID(UUID)をコメントとして付与することでグループ化** し、一括削除できるようにしました。


3. **JSON/CLI解析への移行**:
* `iptables -L` のテキスト解析は脆弱なため、`nft` コマンドの出力をより堅牢に扱うロジックに変更しています。



---

### リファクタリング済みコード: `nft-ipf`

このスクリプトを `nft-ipf` (あるいは `ipf`) として保存し、実行権限を与えてください。

```bash
#!/bin/bash
# Version: 2.0.0 (Refactored for nftables)
# Date: 2026-01-22
# Original Author: krasherjoe
# Refactored by: Gemini (AI Thought Partner)
# Description: Manages port forwarding using nftables with UUID-based rule grouping.

# sudo権限チェック
if [ "$(id -u)" -ne 0 ]; then
    exec sudo "$0" "$@"
fi

# グローバル設定
TABLE_NAME="ipf_wrapper"
QUIET=false
PROTO="tcp"

# ---------------------------------------------------------
# ヘルパー関数
# ---------------------------------------------------------

# コンテナ環境検出 (オリジナルロジック維持)
is_container() {
    if command -v systemd-detect-virt &> /dev/null; then
        if systemd-detect-virt --quiet | grep -qE "container|vm"; then
            return 0
        fi
    fi
    if command -v lxc-checkconfig &> /dev/null; then
        if lxc-checkconfig 2>&1 | grep -q "Running in an LXC container"; then
            return 0
        fi
    fi
    return 1
}

# バージョン表示
show_version() {
    cat << EOF
nft-ipf ver.2.0.0
Date: 2026-01-22
Based on ipf ver.1.0.4

This tool uses 'nftables' instead of 'iptables'.
Rules are managed in a dedicated table named '${TABLE_NAME}'.
EOF
}

# ヘルプ表示
show_help() {
    cat << EOF
Usage: $(basename "$0") [OPTIONS] [PORT:IP:PORT | -L | -d HANDLE]

Examples:
  ipf 11434:10.1.1.2:11434          # Forward local port 11434 to 10.1.1.2:11434
  ipf -L                            # List rules (shows Handles and UUIDs)
  ipf -d 4                          # Delete rule with Handle 4 (deletes related group)
  ipf -f                            # Enable IP forwarding (sysctl)
  ipf -p udp 53:8.8.8.8:53          # Forward UDP

Options:
  -h, --help      Show this help
  -L, -l          List rules
  -d HANDLE       Delete rule by Handle ID (see -L)
  -q              Quiet mode
  -v              Show full nftables ruleset
  -f              Enable IP forwarding
  -p PROTO        Specify protocol (tcp|udp, default: tcp)
  --version       Show version
EOF
}

error() {
    echo -e "\e[31mError: $*\e[0m" >&2
    exit 1
}

# nftablesの初期化(テーブルが存在しなければ作成)
init_nft() {
    if ! command -v nft &> /dev/null; then
        error "nftables (nft command) is not installed."
    fi

    # テーブルとベースチェーンの作成(べき等性を確保)
    nft add table inet ${TABLE_NAME} 2>/dev/null
    
    # チェーンの作成 (priorityは標準的なfilter/natに合わせて設定)
    # PREROUTING: DNAT用
    nft add chain inet ${TABLE_NAME} prerouting { type nat hook prerouting priority dstnat \; } 2>/dev/null
    # OUTPUT: ローカルからのアクセス用 (DNAT)
    nft add chain inet ${TABLE_NAME} output { type nat hook output priority dstnat \; } 2>/dev/null
    # POSTROUTING: Masquerade用
    nft add chain inet ${TABLE_NAME} postrouting { type nat hook postrouting priority srcnat \; } 2>/dev/null
    # FORWARD: 転送許可
    nft add chain inet ${TABLE_NAME} forward { type filter hook forward priority filter \; } 2>/dev/null
}

# ---------------------------------------------------------
# メイン機能
# ---------------------------------------------------------

# ルール一覧表示
list_rules() {
    init_nft
    echo "Forwarding Rules (Table: ${TABLE_NAME}):"
    echo "---------------------------------------------------------------------------------"
    printf "%-8s %-6s %-25s %-25s %-10s\n" "HANDLE" "PROTO" "LOCAL" "TARGET" "UUID"
    echo "---------------------------------------------------------------------------------"

    # PREROUTINGチェーンから主要な転送ルールを抽出して表示
    # 形式: meta l4proto tcp dnat to 10.0.0.1:80 comment "UUID:..."
    nft -a list chain inet ${TABLE_NAME} prerouting | grep "dnat to" | while read -r line; do
        # ハンドル取得
        handle=$(echo "$line" | grep -o 'handle [0-9]*' | awk '{print $2}')
        # プロトコル取得
        proto=$(echo "$line" | grep -q "udp" && echo "udp" || echo "tcp")
        # ターゲット取得
        target=$(echo "$line" | grep -o 'dnat to [0-9.:]*' | awk '{print $3}')
        # ローカルポート取得
        lport=$(echo "$line" | grep -o 'dport [0-9]*' | awk '{print $2}')
        # UUID取得 (コメントから)
        uuid=$(echo "$line" | grep -o 'ipf-id:[a-z0-9-]*' | cut -d':' -f2)

        if [[ -n "$handle" ]]; then
             printf "%-8s %-6s %-25s %-25s %-10s\n" "$handle" "$proto" ":$lport" "$target" "${uuid:0:8}..."
        fi
    done
    echo "---------------------------------------------------------------------------------"
    echo "Use 'ipf -d HANDLE' to delete a rule group."
}

# ルール追加
add_rule() {
    init_nft
    
    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)

    # バリデーション
    if ! [[ "$local_port" =~ ^[0-9]+$ ]] || ! [[ "$target_port" =~ ^[0-9]+$ ]]; then
        error "Invalid ports."
    fi
    if ! [[ "$target_ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
        error "Invalid IP format."
    fi

    # IPフォワード確認
    if [[ $(sysctl -n net.ipv4.ip_forward) -ne 1 ]]; then
        echo -e "\e[33mWarning: IP forwarding is disabled. Run 'ipf -f' to enable.\e[0m"
    fi

    # UUID生成 (ルールセットをグループ化するため)
    local uuid="ipf-id:$(cat /proc/sys/kernel/random/uuid)"
    local comment="comment \"$uuid\""

    # 1. PREROUTING (外部からのDNAT)
    if ! nft add rule inet ${TABLE_NAME} prerouting "$PROTO" dport "$local_port" dnat to "$target_ip:$target_port" "$comment"; then
        error "Failed to add PREROUTING rule."
    fi

    # 2. OUTPUT (ローカルからのDNAT)
    nft add rule inet ${TABLE_NAME} output "$PROTO" dport "$local_port" dnat to "$target_ip:$target_port" "$comment" >/dev/null

    # 3. FORWARD (転送許可)
    # ct state new,established,related accept
    nft add rule inet ${TABLE_NAME} forward ip daddr "$target_ip" "$PROTO" dport "$target_port" ct state new,established,related accept "$comment" >/dev/null
    # 戻りパケット許可 (汎用ルールとして追加してもよいが、ここではグループごとに明示的に許可)
    nft add rule inet ${TABLE_NAME} forward ip saddr "$target_ip" "$PROTO" sport "$target_port" ct state established,related accept "$comment" >/dev/null

    # 4. POSTROUTING (Masquerade - コンテナ環境のみ)
    if is_container; then
        if ! $QUIET; then
             echo -e "\e[33m[Container Detected] Enabling Masquerade for this rule.\e[0m"
        fi
        nft add rule inet ${TABLE_NAME} postrouting ip daddr "$target_ip" "$PROTO" dport "$target_port" masquerade "$comment" >/dev/null
    fi

    if ! $QUIET; then
        echo "Rule added: Local :$local_port -> $target_ip:$target_port ($PROTO)"
    fi
}

# ルール削除
delete_rule() {
    init_nft
    local handle=$1

    if ! [[ "$handle" =~ ^[0-9]+$ ]]; then
        error "Invalid handle number: $handle"
    fi

    # 指定されたハンドルのルールからUUIDを取得 (PREROUTINGにあると仮定)
    local rule_info=$(nft -a list chain inet ${TABLE_NAME} prerouting | grep "handle $handle")
    local uuid=$(echo "$rule_info" | grep -o 'ipf-id:[a-z0-9-]*')

    if [[ -z "$uuid" ]]; then
        error "Rule handle $handle not found or created by external tool (missing ipf-id)."
    fi

    if ! $QUIET; then
        echo "Deleting rule group with UUID: $uuid ..."
    fi

    # そのUUIDを持つすべてのルールをテーブル全体から検索してハンドルのリストを取得
    # 注意: ルールを削除するとハンドルが変わる可能性があるため、一括取得して処理には注意が必要だが
    # nftablesではハンドルは不変。ただし、ループ削除する際は依存関係に注意。
    
    # JSONフォーマットで取得してパースするのが最も確実だが、依存を減らすためgrepで対応
    # テーブル全体のルールを出力し、UUIDにマッチする行のハンドルとチェーンを特定して削除
    
    # シンプルなアプローチ: 各チェーンを走査してUUIDにマッチするハンドルを削除
    for chain in prerouting output forward postrouting; do
        nft -a list chain inet ${TABLE_NAME} "$chain" | grep "$uuid" | grep -o 'handle [0-9]*' | awk '{print $2}' | while read -r h_del; do
            nft delete rule inet ${TABLE_NAME} "$chain" handle "$h_del"
        done
    done

    if ! $QUIET; then
        echo "Rule group deleted."
    fi
}

# カーネルパラメータ設定
enable_forwarding() {
    echo "Enabling IP forwarding..."
    sysctl -w net.ipv4.ip_forward=1
    sysctl -w net.ipv4.conf.all.route_localnet=1
    sysctl -w net.ipv4.conf.default.route_localnet=1
}

# ---------------------------------------------------------
# メイン処理
# ---------------------------------------------------------
while [[ $# -gt 0 ]]; do
    case "$1" in
        -h|--help)
            show_help
            exit 0
            ;;
        -L|-l)
            list_rules
            exit 0
            ;;
        -v)
            nft list ruleset
            exit 0
            ;;
        -f)
            enable_forwarding
            exit 0
            ;;
        --version)
            show_version
            exit 0
            ;;
        -d)
            if [[ $# -lt 2 ]]; then error "Missing handle for -d"; fi
            delete_rule "$2"
            exit 0
            ;;
        -q)
            QUIET=true
            shift
            ;;
        -p)
            if [[ "$2" != "tcp" && "$2" != "udp" ]]; then error "Unsupported protocol"; fi
            PROTO="$2"
            shift 2
            ;;
        -*)
            error "Unknown option: $1"
            ;;
        *)
            break
            ;;
    esac
done

if [[ $# -eq 0 ]]; then
    show_help
    exit 1
fi

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"
fi

```

### 元のスクリプトからの改善・移行ポイント

1. **依存関係の削減**:
* `iptables-save`, `iptables-restore` への依存を排除しました。
* 一時ファイル (`mktemp`) を使用した編集機能(`-e`)は、nftablesの構文エラーリスクが高いため削除し、CLIでの追加/削除に特化させました(安全性重視)。


2. **`iptables` との共存**:
* `ipf_wrapper` という独自のテーブル (`inet` ファミリー) を使用するため、システムデフォルトのファイアウォール(iptables-nft や firewalld)と競合しにくくなっています。


3. **削除ロジックの強化**:
* **旧**: 行番号で削除。DNATルールを消した後、関連するOUTPUTやFORWARDルールを手探りで検索して削除していました(不確実)。
* **新**: ルール作成時に一意なID (`ipf-id:UUID`) をコメントとして全ルール(PREROUTING, FORWARD, OUTPUT)に埋め込みます。削除時はこのIDを持つルールを一括で特定・削除するため、ゴミが残りません。


4. **編集機能の廃止**:
* `-e` オプションは削除しました。nftablesは構造化されているため、テキストエディタでの一括編集よりも、設定ファイル (`/etc/nftables.conf`) での管理またはこのツールでの追加/削除が推奨されるためです。



### 使い方

1. **スクリプトの保存**: `nft-ipf` という名前などで保存します。
2. **実行権限の付与**: `chmod +x nft-ipf`
3. **ルールの追加**:
```bash
# TCP 8080 を 内部の 10.0.0.5:80 に転送
./nft-ipf 8080:10.0.0.5:80

# UDP 5353 を転送
./nft-ipf -p udp 5353:10.0.0.5:53

```


4. **確認と削除**:
```bash
# リスト表示 (HANDLE番号を確認)
./nft-ipf -L

# 削除 (HANDLE番号を指定)
./nft-ipf -d 5

```



もし特定の機能(例:`-e` のテキスト編集機能など)がどうしても必要な場合は、ロジックを追加しますのでお知らせください。
2026-01-22 01:41:29 +00:00

291 lines
No EOL
10 KiB
Bash
Raw 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
# Version: 2.0.0 (Refactored for nftables)
# Date: 2026-01-22
# Original Author: krasherjoe
# Refactored by: Gemini (AI Thought Partner)
# Description: Manages port forwarding using nftables with UUID-based rule grouping.
# sudo権限チェック
if [ "$(id -u)" -ne 0 ]; then
exec sudo "$0" "$@"
fi
# グローバル設定
TABLE_NAME="ipf_wrapper"
QUIET=false
PROTO="tcp"
# ---------------------------------------------------------
# ヘルパー関数
# ---------------------------------------------------------
# コンテナ環境検出 (オリジナルロジック維持)
is_container() {
if command -v systemd-detect-virt &> /dev/null; then
if systemd-detect-virt --quiet | grep -qE "container|vm"; then
return 0
fi
fi
if command -v lxc-checkconfig &> /dev/null; then
if lxc-checkconfig 2>&1 | grep -q "Running in an LXC container"; then
return 0
fi
fi
return 1
}
# バージョン表示
show_version() {
cat << EOF
nft-ipf ver.2.0.0
Date: 2026-01-22
Based on ipf ver.1.0.4
This tool uses 'nftables' instead of 'iptables'.
Rules are managed in a dedicated table named '${TABLE_NAME}'.
EOF
}
# ヘルプ表示
show_help() {
cat << EOF
Usage: $(basename "$0") [OPTIONS] [PORT:IP:PORT | -L | -d HANDLE]
Examples:
ipf 11434:10.1.1.2:11434 # Forward local port 11434 to 10.1.1.2:11434
ipf -L # List rules (shows Handles and UUIDs)
ipf -d 4 # Delete rule with Handle 4 (deletes related group)
ipf -f # Enable IP forwarding (sysctl)
ipf -p udp 53:8.8.8.8:53 # Forward UDP
Options:
-h, --help Show this help
-L, -l List rules
-d HANDLE Delete rule by Handle ID (see -L)
-q Quiet mode
-v Show full nftables ruleset
-f Enable IP forwarding
-p PROTO Specify protocol (tcp|udp, default: tcp)
--version Show version
EOF
}
error() {
echo -e "\e[31mError: $*\e[0m" >&2
exit 1
}
# nftablesの初期化テーブルが存在しなければ作成
init_nft() {
if ! command -v nft &> /dev/null; then
error "nftables (nft command) is not installed."
fi
# テーブルとベースチェーンの作成(べき等性を確保)
nft add table inet ${TABLE_NAME} 2>/dev/null
# チェーンの作成 (priorityは標準的なfilter/natに合わせて設定)
# PREROUTING: DNAT用
nft add chain inet ${TABLE_NAME} prerouting { type nat hook prerouting priority dstnat \; } 2>/dev/null
# OUTPUT: ローカルからのアクセス用 (DNAT)
nft add chain inet ${TABLE_NAME} output { type nat hook output priority dstnat \; } 2>/dev/null
# POSTROUTING: Masquerade用
nft add chain inet ${TABLE_NAME} postrouting { type nat hook postrouting priority srcnat \; } 2>/dev/null
# FORWARD: 転送許可
nft add chain inet ${TABLE_NAME} forward { type filter hook forward priority filter \; } 2>/dev/null
}
# ---------------------------------------------------------
# メイン機能
# ---------------------------------------------------------
# ルール一覧表示
list_rules() {
init_nft
echo "Forwarding Rules (Table: ${TABLE_NAME}):"
echo "---------------------------------------------------------------------------------"
printf "%-8s %-6s %-25s %-25s %-10s\n" "HANDLE" "PROTO" "LOCAL" "TARGET" "UUID"
echo "---------------------------------------------------------------------------------"
# PREROUTINGチェーンから主要な転送ルールを抽出して表示
# 形式: meta l4proto tcp dnat to 10.0.0.1:80 comment "UUID:..."
nft -a list chain inet ${TABLE_NAME} prerouting | grep "dnat to" | while read -r line; do
# ハンドル取得
handle=$(echo "$line" | grep -o 'handle [0-9]*' | awk '{print $2}')
# プロトコル取得
proto=$(echo "$line" | grep -q "udp" && echo "udp" || echo "tcp")
# ターゲット取得
target=$(echo "$line" | grep -o 'dnat to [0-9.:]*' | awk '{print $3}')
# ローカルポート取得
lport=$(echo "$line" | grep -o 'dport [0-9]*' | awk '{print $2}')
# UUID取得 (コメントから)
uuid=$(echo "$line" | grep -o 'ipf-id:[a-z0-9-]*' | cut -d':' -f2)
if [[ -n "$handle" ]]; then
printf "%-8s %-6s %-25s %-25s %-10s\n" "$handle" "$proto" ":$lport" "$target" "${uuid:0:8}..."
fi
done
echo "---------------------------------------------------------------------------------"
echo "Use 'ipf -d HANDLE' to delete a rule group."
}
# ルール追加
add_rule() {
init_nft
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)
# バリデーション
if ! [[ "$local_port" =~ ^[0-9]+$ ]] || ! [[ "$target_port" =~ ^[0-9]+$ ]]; then
error "Invalid ports."
fi
if ! [[ "$target_ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
error "Invalid IP format."
fi
# IPフォワード確認
if [[ $(sysctl -n net.ipv4.ip_forward) -ne 1 ]]; then
echo -e "\e[33mWarning: IP forwarding is disabled. Run 'ipf -f' to enable.\e[0m"
fi
# UUID生成 (ルールセットをグループ化するため)
local uuid="ipf-id:$(cat /proc/sys/kernel/random/uuid)"
local comment="comment \"$uuid\""
# 1. PREROUTING (外部からのDNAT)
if ! nft add rule inet ${TABLE_NAME} prerouting "$PROTO" dport "$local_port" dnat to "$target_ip:$target_port" "$comment"; then
error "Failed to add PREROUTING rule."
fi
# 2. OUTPUT (ローカルからのDNAT)
nft add rule inet ${TABLE_NAME} output "$PROTO" dport "$local_port" dnat to "$target_ip:$target_port" "$comment" >/dev/null
# 3. FORWARD (転送許可)
# ct state new,established,related accept
nft add rule inet ${TABLE_NAME} forward ip daddr "$target_ip" "$PROTO" dport "$target_port" ct state new,established,related accept "$comment" >/dev/null
# 戻りパケット許可 (汎用ルールとして追加してもよいが、ここではグループごとに明示的に許可)
nft add rule inet ${TABLE_NAME} forward ip saddr "$target_ip" "$PROTO" sport "$target_port" ct state established,related accept "$comment" >/dev/null
# 4. POSTROUTING (Masquerade - コンテナ環境のみ)
if is_container; then
if ! $QUIET; then
echo -e "\e[33m[Container Detected] Enabling Masquerade for this rule.\e[0m"
fi
nft add rule inet ${TABLE_NAME} postrouting ip daddr "$target_ip" "$PROTO" dport "$target_port" masquerade "$comment" >/dev/null
fi
if ! $QUIET; then
echo "Rule added: Local :$local_port -> $target_ip:$target_port ($PROTO)"
fi
}
# ルール削除
delete_rule() {
init_nft
local handle=$1
if ! [[ "$handle" =~ ^[0-9]+$ ]]; then
error "Invalid handle number: $handle"
fi
# 指定されたハンドルのルールからUUIDを取得 (PREROUTINGにあると仮定)
local rule_info=$(nft -a list chain inet ${TABLE_NAME} prerouting | grep "handle $handle")
local uuid=$(echo "$rule_info" | grep -o 'ipf-id:[a-z0-9-]*')
if [[ -z "$uuid" ]]; then
error "Rule handle $handle not found or created by external tool (missing ipf-id)."
fi
if ! $QUIET; then
echo "Deleting rule group with UUID: $uuid ..."
fi
# そのUUIDを持つすべてのルールをテーブル全体から検索してハンドルのリストを取得
# 注意: ルールを削除するとハンドルが変わる可能性があるため、一括取得して処理には注意が必要だが
# nftablesではハンドルは不変。ただし、ループ削除する際は依存関係に注意。
# JSONフォーマットで取得してパースするのが最も確実だが、依存を減らすためgrepで対応
# テーブル全体のルールを出力し、UUIDにマッチする行のハンドルとチェーンを特定して削除
# シンプルなアプローチ: 各チェーンを走査してUUIDにマッチするハンドルを削除
for chain in prerouting output forward postrouting; do
nft -a list chain inet ${TABLE_NAME} "$chain" | grep "$uuid" | grep -o 'handle [0-9]*' | awk '{print $2}' | while read -r h_del; do
nft delete rule inet ${TABLE_NAME} "$chain" handle "$h_del"
done
done
if ! $QUIET; then
echo "Rule group deleted."
fi
}
# カーネルパラメータ設定
enable_forwarding() {
echo "Enabling IP forwarding..."
sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv4.conf.all.route_localnet=1
sysctl -w net.ipv4.conf.default.route_localnet=1
}
# ---------------------------------------------------------
# メイン処理
# ---------------------------------------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
show_help
exit 0
;;
-L|-l)
list_rules
exit 0
;;
-v)
nft list ruleset
exit 0
;;
-f)
enable_forwarding
exit 0
;;
--version)
show_version
exit 0
;;
-d)
if [[ $# -lt 2 ]]; then error "Missing handle for -d"; fi
delete_rule "$2"
exit 0
;;
-q)
QUIET=true
shift
;;
-p)
if [[ "$2" != "tcp" && "$2" != "udp" ]]; then error "Unsupported protocol"; fi
PROTO="$2"
shift 2
;;
-*)
error "Unknown option: $1"
;;
*)
break
;;
esac
done
if [[ $# -eq 0 ]]; then
show_help
exit 1
fi
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"
fi