#!/usr/bin/env bash if [[ -n ${IPF_COMMON_SH_LOADED:-} ]]; then return 0 fi IPF_COMMON_SH_LOADED=1 : "${IPF_COLOR_ENABLED:=1}" : "${IPF_FORCE_PLAIN_UI:=0}" : "${IPF_APP_NAME:=IPTables 端口转发管理工具}" if [[ ! -t 1 ]]; then IPF_COLOR_ENABLED=0 fi if [[ ${IPF_COLOR_ENABLED} == 1 ]]; then CLR_RED=$'\033[31m' CLR_GREEN=$'\033[32m' CLR_YELLOW=$'\033[33m' CLR_BLUE=$'\033[34m' CLR_RESET=$'\033[0m' else CLR_RED='' CLR_GREEN='' CLR_YELLOW='' CLR_BLUE='' CLR_RESET='' fi command_is_available() { local cmd=${1:-} if [[ -z ${cmd} ]]; then return 1 fi if [[ ${cmd} == */* ]]; then [[ -x ${cmd} ]] return fi command -v "${cmd}" >/dev/null 2>&1 } ts_now() { date '+%H:%M:%S' } _log_with_color() { local color=$1 local level=$2 shift 2 printf '%s[%s] [%s]%s %s\n' "${color}" "$(ts_now)" "${level}" "${CLR_RESET}" "$*" >&2 } log_info() { _log_with_color "${CLR_BLUE}" INFO "$@"; } log_warn() { _log_with_color "${CLR_YELLOW}" WARN "$@"; } log_err() { _log_with_color "${CLR_RED}" ERROR "$@"; } log_ok() { _log_with_color "${CLR_GREEN}" OK "$@"; } display_width() { local input=${1-} awk -v str="${input}" 'BEGIN { n = split(str, chars, "") width = 0 for (i = 1; i <= n; i++) { if (chars[i] ~ /[ -~]/) { width += 1 } else { width += 2 } } print width }' } repeat_char() { local char=$1 local count=$2 local out='' while (( count > 0 )); do out+="${char}" ((count--)) done printf '%s' "${out}" } term_width() { if [[ -n ${COLUMNS:-} ]]; then printf '%s\n' "${COLUMNS}" return fi if command_is_available tput; then tput cols 2>/dev/null && return fi printf '80\n' } use_box_ui() { [[ ${IPF_FORCE_PLAIN_UI} != 1 ]] || return 1 [[ $(term_width) -ge 60 ]] } pad_right() { local text=${1-} local width=$2 local actual padding actual=$(display_width "${text}") if (( actual >= width )); then printf '%s' "${text}" return fi padding=$((width - actual)) printf '%s%*s' "${text}" "${padding}" '' } box_top() { local width=${1:-60} printf '╔%s╗\n' "$(repeat_char '═' "$((width - 2))")" } box_bottom() { local width=${1:-60} printf '╚%s╝\n' "$(repeat_char '═' "$((width - 2))")" } box_separator() { local width=${1:-60} printf '╠%s╣\n' "$(repeat_char '═' "$((width - 2))")" } box_line() { local width=${1:-60} local text=${2-} local inner=$((width - 4)) printf '║ %s ║\n' "$(pad_right "${text}" "${inner}")" } print_section() { local title=${1-} if use_box_ui; then local width width=$(term_width) if (( width > 78 )); then width=78 fi box_top "${width}" box_line "${width}" "${title}" box_bottom "${width}" else printf '== %s ==\n' "${title}" fi } read_test_input() { READ_TEST_INPUT_RESULT='' if [[ -n ${IPF_TEST_INPUTS-} ]]; then if [[ ${IPF_TEST_INPUTS} == *$'\n'* ]]; then READ_TEST_INPUT_RESULT=${IPF_TEST_INPUTS%%$'\n'*} IPF_TEST_INPUTS=${IPF_TEST_INPUTS#*$'\n'} else READ_TEST_INPUT_RESULT=${IPF_TEST_INPUTS} IPF_TEST_INPUTS='' fi fi } prompt_input() { local prompt=$1 local default_value=${2-} local reply='' if [[ -n ${IPF_TEST_INPUTS-} ]]; then read_test_input reply=${READ_TEST_INPUT_RESULT} if [[ -z ${reply} && -n ${default_value} ]]; then reply=${default_value} fi PROMPT_INPUT_RESULT=${reply} printf '%s\n' "${reply}" >&2 printf '%s\n' "${reply}" return 0 fi if [[ -n ${default_value} ]]; then read -r -p "${prompt} [${default_value}]: " reply reply=${reply:-${default_value}} else read -r -p "${prompt}: " reply fi PROMPT_INPUT_RESULT=${reply} printf '%s\n' "${reply}" } prompt_input_capture() { prompt_input "$@" >/dev/null } prompt_confirm() { local prompt=$1 local default_choice=${2:-n} local answer normalized suffix if [[ ${default_choice} == y ]]; then suffix='[Y/n]' else suffix='[y/N]' fi prompt_input_capture "${prompt} ${suffix}" "" answer=${PROMPT_INPUT_RESULT} if [[ -z ${answer} ]]; then answer=${default_choice} fi normalized=${answer,,} [[ ${normalized} == y || ${normalized} == yes ]] } prompt_select() { local prompt=$1 shift local options=("$@") local idx answer count count=${#options[@]} for ((idx = 0; idx < count; idx++)); do printf ' [%d] %s\n' "$((idx + 1))" "${options[idx]}" >&2 done while true; do prompt_input_capture "${prompt}" "" answer=${PROMPT_INPUT_RESULT} if [[ ${answer} =~ ^[0-9]+$ ]] && (( answer >= 1 && answer <= count )); then PROMPT_SELECT_RESULT=$((answer - 1)) printf '%s\n' "${PROMPT_SELECT_RESULT}" return 0 fi log_warn "请输入 1-${count} 之间的编号。" done } prompt_select_capture() { prompt_select "$@" >/dev/null } pause_return() { if [[ -n ${IPF_TEST_INPUTS-} ]]; then return 0 fi read -r -p '按回车继续...' _ } validate_ipv4() { local ip=${1-} local IFS='.' local -a parts=() local part [[ ${ip} =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1 read -r -a parts <<<"${ip}" for part in "${parts[@]}"; do [[ ${part} =~ ^[0-9]+$ ]] || return 1 (( part >= 0 && part <= 255 )) || return 1 done } _validate_ipv6_part_count() { local part=${1-} local IFS=':' local -a fields=() local field idx count=0 [[ -n ${part} ]] || { printf '0\n' return 0 } [[ ${part} != :* ]] || return 1 [[ ${part} != *: ]] || return 1 [[ ${part} != *::* ]] || return 1 read -r -a fields <<<"${part}" for idx in "${!fields[@]}"; do field=${fields[idx]} [[ -n ${field} ]] || return 1 if [[ ${field} == IPV4TAIL ]]; then (( idx == ${#fields[@]} - 1 )) || return 1 ((count += 2)) continue fi [[ ${field} =~ ^[0-9A-Fa-f]{1,4}$ ]] || return 1 ((count += 1)) done printf '%s\n' "${count}" } validate_ipv6() { local ip=${1-} local base scope tail prefix left right left_count right_count total_count [[ -n ${ip} ]] || return 1 [[ ${ip} == *:* ]] || return 1 scope='' base=${ip} if [[ ${ip} == *%* ]]; then scope=${ip#*%} base=${ip%%\%*} [[ -n ${scope} ]] || return 1 [[ ${scope} =~ ^[A-Za-z0-9_.:-]+$ ]] || return 1 fi if [[ ${base} == *.* ]]; then tail=${base##*:} prefix=${base%:*} [[ ${prefix} != "${base}" ]] || return 1 validate_ipv4 "${tail}" || return 1 base="${prefix}:IPV4TAIL" fi [[ ${base//IPV4TAIL/} =~ ^[0-9A-Fa-f:]+$ ]] || return 1 if [[ ${base} == *::* ]]; then [[ ${base#*::} != *::* ]] || return 1 left=${base%%::*} right=${base#*::} left_count=$(_validate_ipv6_part_count "${left}") || return 1 right_count=$(_validate_ipv6_part_count "${right}") || return 1 total_count=$((left_count + right_count)) (( total_count < 8 )) || return 1 return 0 fi total_count=$(_validate_ipv6_part_count "${base}") || return 1 (( total_count == 8 )) } validate_port() { local port=${1-} [[ ${port} =~ ^[0-9]+$ ]] || return 1 (( port >= 1 && port <= 65535 )) } validate_proto() { case ${1-} in tcp|udp|both) return 0 ;; *) return 1 ;; esac } validate_ipver() { case ${1-} in 4|6|both) return 0 ;; *) return 1 ;; esac } require_root() { if (( EUID != 0 )); then log_err '请使用 root 或 sudo 运行此脚本。' return 2 fi }