392 lines
10 KiB
Bash
392 lines
10 KiB
Bash
#!/usr/bin/env bash
|
||
|
||
if [[ -n ${IPF_RULES_MGR_SH_LOADED:-} ]]; then
|
||
return 0
|
||
fi
|
||
IPF_RULES_MGR_SH_LOADED=1
|
||
|
||
rules_new_uuid() {
|
||
local raw
|
||
if [[ -r /proc/sys/kernel/random/uuid ]]; then
|
||
raw=$(tr -d '-' </proc/sys/kernel/random/uuid)
|
||
printf '%s\n' "${raw:0:8}"
|
||
return 0
|
||
fi
|
||
date '+%s%N' | tail -c 9
|
||
}
|
||
|
||
rules_now_iso() {
|
||
date '+%Y-%m-%dT%H:%M:%S%z'
|
||
}
|
||
|
||
rule_sanitize_desc() {
|
||
local desc=${1-}
|
||
desc=${desc//$'\n'/ }
|
||
desc=${desc//|//}
|
||
printf '%s\n' "${desc}"
|
||
}
|
||
|
||
rule_target_hint() {
|
||
case ${1-} in
|
||
4) printf '请输入目标 IPv4 地址' ;;
|
||
6) printf '请输入目标 IPv6 地址' ;;
|
||
both) printf '请输入目标地址,格式为 IPv4,IPv6' ;;
|
||
*) printf '请输入目标地址' ;;
|
||
esac
|
||
}
|
||
|
||
rule_validate_target() {
|
||
local tip=${1-}
|
||
local ipver=${2-}
|
||
case ${ipver} in
|
||
4)
|
||
validate_ipv4 "${tip}"
|
||
;;
|
||
6)
|
||
validate_ipv6 "${tip}"
|
||
;;
|
||
both)
|
||
[[ ${tip} == *,* ]] || return 1
|
||
validate_ipv4 "${tip%%,*}" || return 1
|
||
validate_ipv6 "${tip#*,}" || return 1
|
||
;;
|
||
*)
|
||
return 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
rule_validate_definition() {
|
||
local proto=${1-}
|
||
local lport=${2-}
|
||
local tip=${3-}
|
||
local tport=${4-}
|
||
local ipver=${5-}
|
||
|
||
validate_proto "${proto}" || {
|
||
log_err '协议无效,仅支持 tcp/udp/both。'
|
||
return 1
|
||
}
|
||
validate_port "${lport}" || {
|
||
log_err '本地端口无效,应为 1-65535。'
|
||
return 1
|
||
}
|
||
validate_port "${tport}" || {
|
||
log_err '目标端口无效,应为 1-65535。'
|
||
return 1
|
||
}
|
||
validate_ipver "${ipver}" || {
|
||
log_err 'IP 版本无效,仅支持 4/6/both。'
|
||
return 1
|
||
}
|
||
rule_validate_target "${tip}" "${ipver}" || {
|
||
log_err '目标地址与 IP 版本不匹配。若选择 both,请使用 IPv4,IPv6。'
|
||
return 1
|
||
}
|
||
}
|
||
|
||
rule_proto_label() {
|
||
case ${1-} in
|
||
tcp) printf 'TCP' ;;
|
||
udp) printf 'UDP' ;;
|
||
both) printf 'TCP/UDP' ;;
|
||
*) printf '%s' "${1-}" ;;
|
||
esac
|
||
}
|
||
|
||
rule_ipver_label() {
|
||
case ${1-} in
|
||
4) printf 'IPv4' ;;
|
||
6) printf 'IPv6' ;;
|
||
both) printf 'IPv4+IPv6' ;;
|
||
*) printf '%s' "${1-}" ;;
|
||
esac
|
||
}
|
||
|
||
rule_target_label() {
|
||
local tip=${1-}
|
||
if [[ ${tip} == *,* ]]; then
|
||
printf '%s | %s' "${tip%%,*}" "${tip#*,}"
|
||
else
|
||
printf '%s' "${tip}"
|
||
fi
|
||
}
|
||
|
||
rule_build_line() {
|
||
local uuid=$1 proto=$2 lport=$3 tip=$4 tport=$5 ipver=$6 desc=${7-}
|
||
local created
|
||
created=$(rules_now_iso)
|
||
desc=$(rule_sanitize_desc "${desc}")
|
||
printf 'uuid=%s|proto=%s|lport=%s|tip=%s|tport=%s|ipver=%s|desc=%s|created=%s\n' \
|
||
"${uuid}" "${proto}" "${lport}" "${tip}" "${tport}" "${ipver}" "${desc}" "${created}"
|
||
}
|
||
|
||
rule_field() {
|
||
storage_parse "$1" "$2"
|
||
}
|
||
|
||
rules_load_lines() {
|
||
mapfile -t RULES_CACHE < <(storage_list)
|
||
}
|
||
|
||
rule_health_mark() {
|
||
local line=${1-}
|
||
local uuid proto lport tip tport ipver
|
||
uuid=$(rule_field "${line}" uuid)
|
||
proto=$(rule_field "${line}" proto)
|
||
lport=$(rule_field "${line}" lport)
|
||
tip=$(rule_field "${line}" tip)
|
||
tport=$(rule_field "${line}" tport)
|
||
ipver=$(rule_field "${line}" ipver)
|
||
|
||
if ipt_rule_healthy "${uuid}" "${proto}" "${lport}" "${tip}" "${tport}" "${ipver}"; then
|
||
printf '✓\n'
|
||
else
|
||
printf '!\n'
|
||
fi
|
||
}
|
||
|
||
render_rules_row() {
|
||
printf '%s %s %s %s %s %s %s %s\n' \
|
||
"$(pad_right "${1-}" 3)" "$(pad_right "${2-}" 10)" "$(pad_right "${3-}" 10)" "$(pad_right "${4-}" 32)" \
|
||
"$(pad_right "${5-}" 10)" "$(pad_right "${6-}" 12)" "$(pad_right "${7-}" 4)" "${8-}"
|
||
}
|
||
|
||
render_rules_plain() {
|
||
local -a lines=("$@")
|
||
local line idx uuid proto lport tip tport ipver desc health
|
||
if ((${#lines[@]} == 0)); then
|
||
printf '当前没有规则。\n'
|
||
return 0
|
||
fi
|
||
|
||
render_rules_row '#' '协议' '本地端口' '目标地址' '目标端口' 'IP版本' '状态' '描述'
|
||
for ((idx = 0; idx < ${#lines[@]}; idx++)); do
|
||
line=${lines[idx]}
|
||
uuid=$(rule_field "${line}" uuid)
|
||
proto=$(rule_proto_label "$(rule_field "${line}" proto)")
|
||
lport=$(rule_field "${line}" lport)
|
||
tip=$(rule_target_label "$(rule_field "${line}" tip)")
|
||
tport=$(rule_field "${line}" tport)
|
||
ipver=$(rule_ipver_label "$(rule_field "${line}" ipver)")
|
||
desc=$(rule_field "${line}" desc || true)
|
||
health=$(rule_health_mark "${line}")
|
||
render_rules_row "$((idx + 1))" "${proto}" "${lport}" "${tip}" "${tport}" "${ipver}" "${health}" "${desc}"
|
||
done
|
||
}
|
||
|
||
cmd_list() {
|
||
local pause=${1:-1}
|
||
rules_load_lines
|
||
render_rules_plain "${RULES_CACHE[@]}"
|
||
printf '[✓] 运行态规则完整;[!] 至少缺少一条运行态规则或仅存在于数据库中。\n'
|
||
if [[ ${pause} == 1 ]]; then
|
||
pause_return
|
||
fi
|
||
}
|
||
|
||
_cmd_add_batch_locked() {
|
||
local proto=${1-}
|
||
local lport=${2-}
|
||
local tip=${3-}
|
||
local tport=${4-}
|
||
local ipver=${5-}
|
||
local desc=${6-}
|
||
local uuid line
|
||
|
||
rule_validate_definition "${proto}" "${lport}" "${tip}" "${tport}" "${ipver}" || return 1
|
||
|
||
uuid=$(rules_new_uuid)
|
||
line=$(rule_build_line "${uuid}" "${proto}" "${lport}" "${tip}" "${tport}" "${ipver}" "${desc}")
|
||
|
||
if ! ipt_apply_rule "${uuid}" "${proto}" "${lport}" "${tip}" "${tport}" "${ipver}"; then
|
||
log_err 'iptables 规则写入失败。'
|
||
return 1
|
||
fi
|
||
|
||
if ! storage_add_unlocked "${line}"; then
|
||
ipt_remove_rule "${uuid}" "${proto}" "${lport}" "${tip}" "${tport}" "${ipver}" >/dev/null 2>&1 || true
|
||
log_err '写入规则数据库失败。'
|
||
return 1
|
||
fi
|
||
|
||
if ! persist_save; then
|
||
storage_delete_unlocked "${uuid}" >/dev/null 2>&1 || true
|
||
ipt_remove_rule "${uuid}" "${proto}" "${lport}" "${tip}" "${tport}" "${ipver}" >/dev/null 2>&1 || true
|
||
log_err '保存持久化规则失败,已回滚。'
|
||
return 1
|
||
fi
|
||
|
||
log_ok "规则已添加,UUID=${uuid}。"
|
||
printf '%s\n' "${uuid}"
|
||
}
|
||
|
||
cmd_add_batch() {
|
||
storage_with_lock _cmd_add_batch_locked "$@"
|
||
}
|
||
|
||
cmd_add() {
|
||
local proto_choice ipver_choice proto ipver lport tip tport desc
|
||
|
||
print_section '添加新的转发规则'
|
||
prompt_select_capture '请选择协议' 'TCP' 'UDP' 'TCP + UDP'
|
||
proto_choice=${PROMPT_SELECT_RESULT}
|
||
case ${proto_choice} in
|
||
0) proto=tcp ;;
|
||
1) proto=udp ;;
|
||
2) proto=both ;;
|
||
*) return 1 ;;
|
||
esac
|
||
|
||
while true; do
|
||
prompt_input_capture '请输入本地监听端口' ''
|
||
lport=${PROMPT_INPUT_RESULT}
|
||
validate_port "${lport}" && break
|
||
log_warn '端口无效,请重新输入。'
|
||
done
|
||
|
||
prompt_select_capture '请选择 IP 版本' '仅 IPv4' '仅 IPv6' '同时 IPv4 + IPv6'
|
||
ipver_choice=${PROMPT_SELECT_RESULT}
|
||
case ${ipver_choice} in
|
||
0) ipver=4 ;;
|
||
1) ipver=6 ;;
|
||
2) ipver=both ;;
|
||
*) return 1 ;;
|
||
esac
|
||
|
||
while true; do
|
||
prompt_input_capture "$(rule_target_hint "${ipver}")" ''
|
||
tip=${PROMPT_INPUT_RESULT}
|
||
rule_validate_target "${tip}" "${ipver}" && break
|
||
log_warn '目标地址无效,请重新输入。'
|
||
done
|
||
|
||
while true; do
|
||
prompt_input_capture '请输入目标端口' ''
|
||
tport=${PROMPT_INPUT_RESULT}
|
||
validate_port "${tport}" && break
|
||
log_warn '目标端口无效,请重新输入。'
|
||
done
|
||
|
||
prompt_input_capture '请输入描述(可留空)' ''
|
||
desc=${PROMPT_INPUT_RESULT}
|
||
desc=$(rule_sanitize_desc "${desc}")
|
||
|
||
printf '协议: %s\n' "$(rule_proto_label "${proto}")"
|
||
printf '监听端口: %s\n' "${lport}"
|
||
printf '目标地址: %s\n' "$(rule_target_label "${tip}")"
|
||
printf '目标端口: %s\n' "${tport}"
|
||
printf 'IP 版本: %s\n' "$(rule_ipver_label "${ipver}")"
|
||
printf '描述: %s\n' "${desc:-(空)}"
|
||
|
||
if ! prompt_confirm '确认添加该规则?' y; then
|
||
log_warn '已取消添加。'
|
||
return 1
|
||
fi
|
||
|
||
cmd_add_batch "${proto}" "${lport}" "${tip}" "${tport}" "${ipver}" "${desc}" >/dev/null
|
||
}
|
||
|
||
_cmd_delete_uuid_locked() {
|
||
local uuid=${1-}
|
||
local line proto lport tip tport ipver
|
||
line=$(storage_get "${uuid}") || {
|
||
log_err '未找到指定 UUID 的规则。'
|
||
return 1
|
||
}
|
||
|
||
proto=$(rule_field "${line}" proto)
|
||
lport=$(rule_field "${line}" lport)
|
||
tip=$(rule_field "${line}" tip)
|
||
tport=$(rule_field "${line}" tport)
|
||
ipver=$(rule_field "${line}" ipver)
|
||
|
||
if ! ipt_remove_rule "${uuid}" "${proto}" "${lport}" "${tip}" "${tport}" "${ipver}"; then
|
||
log_err '删除 iptables 规则失败。'
|
||
return 1
|
||
fi
|
||
|
||
if ! storage_delete_unlocked "${uuid}"; then
|
||
ipt_apply_rule "${uuid}" "${proto}" "${lport}" "${tip}" "${tport}" "${ipver}" >/dev/null 2>&1 || true
|
||
log_err '删除规则数据库记录失败,已尝试回滚。'
|
||
return 1
|
||
fi
|
||
|
||
if ! persist_save; then
|
||
storage_add_unlocked "${line}" >/dev/null 2>&1 || true
|
||
ipt_apply_rule "${uuid}" "${proto}" "${lport}" "${tip}" "${tport}" "${ipver}" >/dev/null 2>&1 || true
|
||
log_err '持久化保存失败,已尝试回滚。'
|
||
return 1
|
||
fi
|
||
|
||
log_ok "规则 ${uuid} 已删除。"
|
||
}
|
||
|
||
cmd_delete_uuid() {
|
||
storage_with_lock _cmd_delete_uuid_locked "$@"
|
||
}
|
||
|
||
cmd_delete() {
|
||
local index answer line uuid
|
||
rules_load_lines
|
||
if ((${#RULES_CACHE[@]} == 0)); then
|
||
log_info '当前没有可删除的规则。'
|
||
pause_return
|
||
return 0
|
||
fi
|
||
|
||
cmd_list 0
|
||
|
||
while true; do
|
||
prompt_input_capture '请输入要删除的规则编号' ''
|
||
answer=${PROMPT_INPUT_RESULT}
|
||
if [[ ${answer} =~ ^[0-9]+$ ]] && (( answer >= 1 && answer <= ${#RULES_CACHE[@]} )); then
|
||
index=$((answer - 1))
|
||
break
|
||
fi
|
||
log_warn '编号无效,请重新输入。'
|
||
done
|
||
|
||
line=${RULES_CACHE[index]}
|
||
uuid=$(rule_field "${line}" uuid)
|
||
printf '即将删除: %s -> %s:%s (%s / %s)\n' \
|
||
"$(rule_field "${line}" lport)" \
|
||
"$(rule_target_label "$(rule_field "${line}" tip)")" \
|
||
"$(rule_field "${line}" tport)" \
|
||
"$(rule_proto_label "$(rule_field "${line}" proto)")" \
|
||
"$(rule_ipver_label "$(rule_field "${line}" ipver)")"
|
||
|
||
if ! prompt_confirm '确认删除该规则?' n; then
|
||
log_warn '已取消删除。'
|
||
return 1
|
||
fi
|
||
|
||
cmd_delete_uuid "${uuid}"
|
||
}
|
||
|
||
cmd_show_env_status() {
|
||
print_section '环境状态'
|
||
env_check_collect_issues
|
||
env_check_print_report
|
||
printf '规则总数: %s\n' "$(storage_count)"
|
||
if persist_available; then
|
||
printf '持久化: 可用\n'
|
||
else
|
||
printf '持久化: 不可用\n'
|
||
fi
|
||
pause_return
|
||
}
|
||
|
||
_cmd_save_rules_locked() {
|
||
if persist_save; then
|
||
log_ok '规则已保存到磁盘。'
|
||
else
|
||
log_err '规则保存失败。'
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
cmd_save_rules() {
|
||
storage_with_lock _cmd_save_rules_locked
|
||
}
|