#!/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-} local char code width=0 while IFS= read -r -n1 char; do code=$(printf '%d' "'${char}") if (( code >= 32 && code <= 126 )); then ((width += 1)) else ((width += 2)) fi done < <(printf '%s' "${input}") printf '%s\n' "${width}" } repeat_char() { local char=$1 local count=$2 local out='' while (( count > 0 )); do out+="${char}" ((count--)) done printf '%s' "${out}" } term_width() { [[ -n ${COLUMNS:-} ]] && { printf '%s\n' "${COLUMNS}"; return; } command_is_available tput && tput cols 2>/dev/null && return printf '80\n' } use_box_ui() { [[ ${IPF_FORCE_PLAIN_UI} != 1 && $(term_width) -ge 60 ]] } box_width() { local width width=$(term_width) (( width > 78 )) && width=78 printf '%s\n' "${width}" } 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_border() { local left=$1 right=$2 width=${3:-60} printf '%s%s%s\n' "${left}" "$(repeat_char '═' "$((width - 2))")" "${right}" } box_top() { box_border '╔' '╗' "${1:-60}"; } box_bottom() { box_border '╚' '╝' "${1:-60}"; } box_separator() { box_border '╠' '╣' "${1:-60}"; } 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-} width if use_box_ui; then width=$(box_width) box_top "${width}" box_line "${width}" "${title}" box_bottom "${width}" return fi printf '== %s ==\n' "${title}" } 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 [[ -n ${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() { [[ ${1-} =~ ^(tcp|udp|both)$ ]] } validate_ipver() { [[ ${1-} =~ ^(4|6|both)$ ]] } require_root() { if (( EUID != 0 )); then log_err '请使用 root 或 sudo 运行此脚本。' return 2 fi }