#!/usr/bin/env bash # Copyright (c) 2021-2025 community-scripts ORG # Author: MickLesk (Canbiz) && Desert_Gamer # License: MIT function header_info { clear cat <<"EOF" ___ ____ _____ |_ _| _ \ _ |_ _|_ _ __ _ | || |_) (_) | |/ _` |/ _` | | || __/ _ | | (_| | (_| | |___|_| (_) |_|\__,_|\__, | |___/ EOF } clear header_info APP="IP-Tag" hostname=$(hostname) # Color variables YW=$(echo "\033[33m") GN=$(echo "\033[1;92m") RD=$(echo "\033[01;31m") CL=$(echo "\033[m") BFR="\\r\\033[K" HOLD=" " CM="${GN}✓${CL} " CROSS="${RD}✗${CL} " # Stop any running spinner stop_spinner() { if [ -n "$SPINNER_PID" ] && kill -0 "$SPINNER_PID" 2>/dev/null; then kill -TERM "$SPINNER_PID" 2>/dev/null wait "$SPINNER_PID" 2>/dev/null fi SPINNER_PID="" printf "\e[?25h\r" } # Error handler for displaying error messages error_handler() { stop_spinner local exit_code="$?" local line_number="$1" local command="$2" local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" echo -e "\n$error_message\n" } # Spinner for progress indication spinner() { local msg="$1" local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') local spin_i=0 local interval=0.1 trap 'exit 0' TERM INT printf "\e[?25l" 2>/dev/null while true; do printf "\r%s ${YW}%s${CL}" "${frames[spin_i]}" "$msg" 2>/dev/null || exit 0 spin_i=$(((spin_i + 1) % ${#frames[@]})) sleep "$interval" || exit 0 done } # Info message msg_info() { local msg="$1" stop_spinner spinner "$msg" & SPINNER_PID=$! disown $SPINNER_PID 2>/dev/null } # Success message msg_ok() { stop_spinner local msg="$1" echo -e "${BFR}${CM}${GN}${msg}${CL}" } # Error message msg_error() { stop_spinner local msg="$1" echo -e "${BFR}${CROSS}${RD}${msg}${CL}" } # Migrate configuration from old path to new migrate_config() { local old_config="/opt/lxc-iptag" local new_config="/opt/iptag/iptag.conf" if [[ -f "$old_config" ]]; then msg_info "Migrating configuration from old path" if cp "$old_config" "$new_config" &>/dev/null; then rm -rf "$old_config" &>/dev/null msg_ok "Configuration migrated and old config removed" else msg_error "Failed to migrate configuration" fi fi } # Update existing installation update_installation() { msg_info "Updating IP-Tag Scripts" systemctl stop iptag.service &>/dev/null msg_ok "Stopped IP-Tag service" # Create directory if it doesn't exist if [[ ! -d "/opt/iptag" ]]; then mkdir -p /opt/iptag fi # Create new config file (check if exists and ask user) if [[ -f "/opt/iptag/iptag.conf" ]]; then echo -e "\n${YW}Configuration file already exists.${CL}" echo -e "${YW}Note: No critical changes were made in this version.${CL}" while true; do read -p "Do you want to replace it with defaults? (y/n): " yn case $yn in [Yy]*) interactive_config_setup msg_info "Replacing configuration file" generate_config >/opt/iptag/iptag.conf msg_ok "Configuration file replaced with defaults" break ;; [Nn]*) echo -e "${GN}✓ Keeping existing configuration file${CL}" break ;; *) echo -e "${RD}Please answer yes or no.${CL}" ;; esac done else interactive_config_setup msg_info "Creating new configuration file" generate_config >/opt/iptag/iptag.conf msg_ok "Created new configuration file at /opt/iptag/iptag.conf" fi # Update main script msg_info "Updating main script" generate_main_script >/opt/iptag/iptag chmod +x /opt/iptag/iptag msg_ok "Updated main script" # Update service file msg_info "Updating service file" generate_service >/lib/systemd/system/iptag.service msg_ok "Updated service file" msg_info "Creating manual run command" cat <<'EOF' >/usr/local/bin/iptag-run #!/usr/bin/env bash CONFIG_FILE="/opt/iptag/iptag.conf" SCRIPT_FILE="/opt/iptag/iptag" if [[ ! -f "$SCRIPT_FILE" ]]; then echo "✗ Main script not found: $SCRIPT_FILE" exit 1 fi export FORCE_SINGLE_RUN=true exec "$SCRIPT_FILE" EOF chmod +x /usr/local/bin/iptag-run msg_ok "Created iptag-run executable - You can execute this manually by entering “iptag-run” in the Proxmox host, so the script is executed by hand." msg_info "Restarting service" systemctl daemon-reload &>/dev/null systemctl enable -q --now iptag.service &>/dev/null msg_ok "Updated IP-Tag Scripts" # Show configuration information after update show_post_install_info } # Install only command without service install_command_only() { msg_info "Installing IP-Tag Command Only" # Create directory if it doesn't exist if [[ ! -d "/opt/iptag" ]]; then mkdir -p /opt/iptag fi # Migrate config if needed migrate_config # Interactive configuration setup if [[ ! -f /opt/iptag/iptag.conf ]]; then interactive_config_setup_command msg_info "Setup Configuration" generate_config >/opt/iptag/iptag.conf msg_ok "Created configuration file at /opt/iptag/iptag.conf" else stop_spinner echo -e "\n${YW}Configuration file already exists.${CL}" read -p "Do you want to reconfigure tag format? (y/n): " reconfigure case $reconfigure in [Yy]*) interactive_config_setup_command msg_info "Updating Configuration" generate_config >/opt/iptag/iptag.conf msg_ok "Updated configuration file" ;; *) msg_ok "Keeping existing configuration file" ;; esac fi # Setup main script msg_info "Setup Main Script" generate_main_script >/opt/iptag/iptag chmod +x /opt/iptag/iptag msg_ok "Created main script" # Create manual run command msg_info "Creating iptag-run command" cat <<'EOF' >/usr/local/bin/iptag-run #!/usr/bin/env bash CONFIG_FILE="/opt/iptag/iptag.conf" SCRIPT_FILE="/opt/iptag/iptag" if [[ ! -f "$SCRIPT_FILE" ]]; then echo "✗ Main script not found: $SCRIPT_FILE" exit 1 fi export FORCE_SINGLE_RUN=true exec "$SCRIPT_FILE" EOF chmod +x /usr/local/bin/iptag-run msg_ok "Created iptag-run command" msg_ok "IP-Tag Command installed successfully! Use 'iptag-run' to run manually." } # Show post-installation information show_post_install_info() { stop_spinner echo -e "\n${YW}=== Next Steps ===${CL}" # Show usage information if command -v iptag-run >/dev/null 2>&1; then echo -e "${YW}Run IP tagging manually: ${GN}iptag-run${CL}" echo -e "${YW}Add to cron for scheduled execution if needed${CL}" echo -e "" fi echo -e "${RD}IMPORTANT: Configure your network subnets!${CL}" echo -e "" echo -e "${YW}Configuration file: ${GN}/opt/iptag/iptag.conf${CL}" echo -e "" echo -e "${YW}Edit CIDR_LIST with your actual subnets:${CL}" echo -e "${GN}nano /opt/iptag/iptag.conf${CL} ${YW}or${CL} ${GN}vim /opt/iptag/iptag.conf${CL}" echo -e "" echo -e "${YW}Example configuration:${CL}" echo -e "${GN}CIDR_LIST=(${CL}" echo -e "${GN} 192.168.1.0/24 # Your actual subnet${CL}" echo -e "${GN} 10.10.0.0/16 # Another subnet${CL}" echo -e "${GN})${CL}" echo -e "" } # Interactive configuration setup for command-only (TAG_FORMAT only) interactive_config_setup_command() { echo -e "\n${YW}=== Configuration Setup ===${CL}" # TAG_FORMAT configuration echo -e "\n${YW}Select tag format:${CL}" echo -e "${GN}1)${CL} last_two_octets - Show last two octets (e.g., 0.100) [Default]" echo -e "${GN}2)${CL} last_octet - Show only last octet (e.g., 100)" echo -e "${GN}3)${CL} full - Show full IP address (e.g., 192.168.0.100)" while true; do read -p "Enter your choice (1-3) [1]: " tag_choice case ${tag_choice:-1} in 1) TAG_FORMAT="last_two_octets" echo -e "${GN}✓ Selected: last_two_octets${CL}" break ;; 2) TAG_FORMAT="last_octet" echo -e "${GN}✓ Selected: last_octet${CL}" break ;; 3) TAG_FORMAT="full" echo -e "${GN}✓ Selected: full${CL}" break ;; *) echo -e "${RD}Please enter 1, 2, or 3.${CL}" ;; esac done # Set default LOOP_INTERVAL for command mode LOOP_INTERVAL=300 } # Interactive configuration setup for service (TAG_FORMAT + LOOP_INTERVAL) interactive_config_setup() { echo -e "\n${YW}=== Configuration Setup ===${CL}" # TAG_FORMAT configuration echo -e "\n${YW}Select tag format:${CL}" echo -e "${GN}1)${CL} last_two_octets - Show last two octets (e.g., 0.100) [Default]" echo -e "${GN}2)${CL} last_octet - Show only last octet (e.g., 100)" echo -e "${GN}3)${CL} full - Show full IP address (e.g., 192.168.0.100)" while true; do read -p "Enter your choice (1-3) [1]: " tag_choice case ${tag_choice:-1} in 1) TAG_FORMAT="last_two_octets" echo -e "${GN}✓ Selected: last_two_octets${CL}" break ;; 2) TAG_FORMAT="last_octet" echo -e "${GN}✓ Selected: last_octet${CL}" break ;; 3) TAG_FORMAT="full" echo -e "${GN}✓ Selected: full${CL}" break ;; *) echo -e "${RD}Please enter 1, 2, or 3.${CL}" ;; esac done # LOOP_INTERVAL configuration echo -e "\n${YW}Set check interval (in seconds):${CL}" echo -e "${YW}Default: 300 seconds (5 minutes)${CL}" echo -e "${YW}Recommended range: 300-3600 seconds${CL}" while true; do read -p "Enter interval in seconds [300]: " interval_input interval_input=${interval_input:-300} if [[ $interval_input =~ ^[0-9]+$ ]] && [ $interval_input -ge 300 ] && [ $interval_input -le 7200 ]; then LOOP_INTERVAL=$interval_input echo -e "${GN}✓ Selected: ${LOOP_INTERVAL} seconds${CL}" break else echo -e "${RD}Please enter a valid number between 300 and 7200 seconds.${CL}" fi done } # Generate configuration file content generate_config() { cat <&2 fi } # Color constants readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' readonly YELLOW='\033[0;33m' readonly BLUE='\033[0;34m' readonly PURPLE='\033[0;35m' readonly CYAN='\033[0;36m' readonly WHITE='\033[1;37m' readonly GRAY='\033[0;37m' readonly NC='\033[0m' # No Color # Logging functions with colors log_success() { echo -e "${GREEN}✓${NC} $*" } log_info() { echo -e "${BLUE}ℹ${NC} $*" } log_warning() { echo -e "${YELLOW}⚠${NC} $*" } log_error() { echo -e "${RED}✗${NC} $*" } log_change() { echo -e "${CYAN}~${NC} $*" } log_unchanged() { echo -e "${GRAY}=${NC} $*" } # Check if IP is in CIDR ip_in_cidr() { local ip="$1" cidr="$2" debug_log "ip_in_cidr: checking '$ip' against '$cidr'" # Manual CIDR check - более надёжный метод debug_log "ip_in_cidr: using manual check (bypassing ipcalc)" local network prefix IFS='/' read -r network prefix <<< "$cidr" # Convert IP and network to integers for comparison local ip_int net_int mask IFS='.' read -r a b c d <<< "$ip" ip_int=$(( (a << 24) + (b << 16) + (c << 8) + d )) IFS='.' read -r a b c d <<< "$network" net_int=$(( (a << 24) + (b << 16) + (c << 8) + d )) # Create subnet mask mask=$(( 0xFFFFFFFF << (32 - prefix) )) # Apply mask and compare local ip_masked=$((ip_int & mask)) local net_masked=$((net_int & mask)) debug_log "ip_in_cidr: IP=$ip ($ip_int), Network=$network ($net_int), Prefix=$prefix" debug_log "ip_in_cidr: Mask=$mask (hex: $(printf '0x%08x' $mask))" debug_log "ip_in_cidr: IP&Mask=$ip_masked ($(printf '%d.%d.%d.%d' $((ip_masked>>24&255)) $((ip_masked>>16&255)) $((ip_masked>>8&255)) $((ip_masked&255))))" debug_log "ip_in_cidr: Net&Mask=$net_masked ($(printf '%d.%d.%d.%d' $((net_masked>>24&255)) $((net_masked>>16&255)) $((net_masked>>8&255)) $((net_masked&255))))" if (( ip_masked == net_masked )); then debug_log "ip_in_cidr: manual check PASSED - IP is in CIDR" return 0 else debug_log "ip_in_cidr: manual check FAILED - IP is NOT in CIDR" return 1 fi } # Format IP address according to the configuration format_ip_tag() { local ip="$1" [[ -z "$ip" ]] && return local format="${TAG_FORMAT:-$DEFAULT_TAG_FORMAT}" case "$format" in "last_octet") echo "${ip##*.}" ;; "last_two_octets") echo "${ip#*.*.}" ;; *) echo "$ip" ;; esac } # Check if IP is in any CIDRs ip_in_cidrs() { local ip="$1" cidrs="$2" [[ -z "$cidrs" ]] && return 1 local IFS=' ' debug_log "Checking IP '$ip' against CIDRs: '$cidrs'" for cidr in $cidrs; do debug_log "Testing IP '$ip' against CIDR '$cidr'" if ip_in_cidr "$ip" "$cidr"; then debug_log "IP '$ip' matches CIDR '$cidr' - PASSED" return 0 else debug_log "IP '$ip' does not match CIDR '$cidr'" fi done debug_log "IP '$ip' failed all CIDR checks" return 1 } # Check if IP is valid is_valid_ipv4() { local ip="$1" [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1 local IFS='.' parts read -ra parts <<< "$ip" for part in "${parts[@]}"; do (( part >= 0 && part <= 255 )) || return 1 done return 0 } # Get VM IPs using improved methods get_vm_ips() { local vmid=$1 ips="" local vm_config="/etc/pve/qemu-server/${vmid}.conf" [[ ! -f "$vm_config" ]] && return debug_log "vm $vmid: starting IP detection" # Check if VM is running first local vm_status="" if command -v qm >/dev/null 2>&1; then vm_status=$(qm status "$vmid" 2>/dev/null | awk '{print $2}') fi if [[ "$vm_status" != "running" ]]; then debug_log "vm $vmid: not running (status: $vm_status)" return fi # Get MAC addresses from config local mac_addresses=$(grep -E "^net[0-9]+:" "$vm_config" | grep -oE "([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}" | head -3) debug_log "vm $vmid: found MACs: $mac_addresses" # Method 1: QM guest agent (most reliable for current IP) if command -v qm >/dev/null 2>&1; then debug_log "vm $vmid: trying qm guest agent first" local qm_ips=$(timeout 8 qm guest cmd "$vmid" network-get-interfaces 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | grep -v "127.0.0.1" | head -3) for qm_ip in $qm_ips; do if [[ "$qm_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then debug_log "vm $vmid: found IP $qm_ip via qm guest cmd" ips+="$qm_ip " fi done fi # Method 2: Fresh ARP table lookup (force refresh) if [[ -n "$mac_addresses" ]]; then debug_log "vm $vmid: refreshing ARP table and checking" # Try to refresh ARP table by pinging network ranges for mac in $mac_addresses; do local mac_lower=$(echo "$mac" | tr '[:upper:]' '[:lower:]') # First check current ARP table local current_ip=$(ip neighbor show | grep "$mac_lower" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1) # If found in ARP, verify it's still valid by trying to ping if [[ -n "$current_ip" && "$current_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then debug_log "vm $vmid: found IP $current_ip in ARP table for MAC $mac_lower, verifying..." # Quick ping test to verify IP is still active if timeout 2 ping -c 1 "$current_ip" >/dev/null 2>&1; then debug_log "vm $vmid: verified IP $current_ip is active via ping" ips+="$current_ip " else debug_log "vm $vmid: IP $current_ip failed ping verification, removing from ARP" # Remove stale ARP entry ip neighbor del "$current_ip" dev $(ip route get "$current_ip" 2>/dev/null | grep -oE 'dev [^ ]+' | cut -d' ' -f2) 2>/dev/null || true fi fi done fi # Method 3: DHCP leases (backup method) if [[ -z "$ips" ]]; then debug_log "vm $vmid: checking DHCP leases as fallback" for mac in $mac_addresses; do local mac_lower=$(echo "$mac" | tr '[:upper:]' '[:lower:]') for dhcp_file in "/var/lib/dhcp/dhcpd.leases" "/var/lib/dhcpcd5/dhcpcd.leases"; do if [[ -f "$dhcp_file" ]]; then # Look for most recent lease for this MAC local dhcp_ip=$(tac "$dhcp_file" 2>/dev/null | grep -A 10 "ethernet $mac_lower" | grep "binding state active" -A 5 | grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}" | head -1) if [[ -n "$dhcp_ip" && "$dhcp_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then debug_log "vm $vmid: found IP $dhcp_ip via DHCP leases" # Verify this IP responds if timeout 2 ping -c 1 "$dhcp_ip" >/dev/null 2>&1; then debug_log "vm $vmid: verified DHCP IP $dhcp_ip is active" ips+="$dhcp_ip " break 2 else debug_log "vm $vmid: DHCP IP $dhcp_ip failed ping test" fi fi fi done done fi # Remove duplicates and clean up local unique_ips=$(echo "$ips" | tr ' ' '\n' | sort -u | tr '\n' ' ') unique_ips="${unique_ips% }" debug_log "vm $vmid: final IPs: '$unique_ips'" echo "$unique_ips" } # Update tags for container or VM update_tags() { local type="$1" vmid="$2" local current_ips_full if [[ "$type" == "lxc" ]]; then current_ips_full=$(get_lxc_ips "${vmid}") while IFS= read -r line; do [[ "$line" == tags:* ]] && current_tags_raw="${line#tags: }" && break done < <(pct config "$vmid" 2>/dev/null) else current_ips_full=$(get_vm_ips "${vmid}") local vm_config="/etc/pve/qemu-server/${vmid}.conf" if [[ -f "$vm_config" ]]; then local current_tags_raw=$(grep "^tags:" "$vm_config" 2>/dev/null | cut -d: -f2 | sed 's/^[[:space:]]*//') fi fi local current_tags=() next_tags=() current_ip_tags=() if [[ -n "$current_tags_raw" ]]; then mapfile -t current_tags < <(echo "$current_tags_raw" | sed 's/;/\n/g') fi # Separate IP/numeric and user tags for tag in "${current_tags[@]}"; do if is_valid_ipv4 "${tag}" || [[ "$tag" =~ ^[0-9]+(\.[0-9]+)*$ ]]; then current_ip_tags+=("${tag}") else next_tags+=("${tag}") fi done # Generate new IP tags from current IPs local formatted_ips=() debug_log "$type $vmid current_ips_full: '$current_ips_full'" debug_log "$type $vmid CIDR_LIST: ${CIDR_LIST[*]}" for ip in $current_ips_full; do [[ -z "$ip" ]] && continue debug_log "$type $vmid processing IP: '$ip'" if is_valid_ipv4 "$ip"; then debug_log "$type $vmid IP '$ip' is valid" if ip_in_cidrs "$ip" "${CIDR_LIST[*]}"; then debug_log "$type $vmid IP '$ip' passed CIDR check" local formatted_ip=$(format_ip_tag "$ip") debug_log "$type $vmid formatted '$ip' -> '$formatted_ip'" [[ -n "$formatted_ip" ]] && formatted_ips+=("$formatted_ip") else debug_log "$type $vmid IP '$ip' failed CIDR check" fi else debug_log "$type $vmid IP '$ip' is invalid" fi done debug_log "$type $vmid final formatted_ips: ${formatted_ips[*]}" # If LXC and no IPs detected, do not touch tags at all if [[ "$type" == "lxc" && ${#formatted_ips[@]} -eq 0 ]]; then log_unchanged "LXC ${GRAY}${vmid}${NC}: No IP detected, tags unchanged" return fi # Prepend new IP tags to the beginning of the tag list local final_tags=() for new_ip in "${formatted_ips[@]}"; do final_tags+=("$new_ip") done for tag in "${next_tags[@]}"; do final_tags+=("$tag") done next_tags=("${final_tags[@]}") # Update tags if there are changes local old_tags_str=$(IFS=';'; echo "${current_tags[*]}") local new_tags_str=$(IFS=';'; echo "${next_tags[*]}") debug_log "$type $vmid old_tags: '$old_tags_str'" debug_log "$type $vmid new_tags: '$new_tags_str'" debug_log "$type $vmid tags_equal: $([[ "$old_tags_str" == "$new_tags_str" ]] && echo true || echo false)" if [[ "$old_tags_str" != "$new_tags_str" ]]; then # Determine what changed local old_ip_tags_count=${#current_ip_tags[@]} local new_ip_tags_count=${#formatted_ips[@]} # Build detailed change message local change_details="" if [[ $old_ip_tags_count -eq 0 ]]; then change_details="added ${new_ip_tags_count} IP tag(s): [${GREEN}${formatted_ips[*]}${NC}]" else # Compare old and new IP tags local added_tags=() removed_tags=() common_tags=() # Find removed tags for old_tag in "${current_ip_tags[@]}"; do local found=false for new_tag in "${formatted_ips[@]}"; do if [[ "$old_tag" == "$new_tag" ]]; then found=true break fi done if [[ "$found" == false ]]; then removed_tags+=("$old_tag") else common_tags+=("$old_tag") fi done # Find added tags for new_tag in "${formatted_ips[@]}"; do local found=false for old_tag in "${current_ip_tags[@]}"; do if [[ "$new_tag" == "$old_tag" ]]; then found=true break fi done if [[ "$found" == false ]]; then added_tags+=("$new_tag") fi done # Build change message local change_parts=() if [[ ${#added_tags[@]} -gt 0 ]]; then change_parts+=("added [${GREEN}${added_tags[*]}${NC}]") fi if [[ ${#removed_tags[@]} -gt 0 ]]; then change_parts+=("removed [${YELLOW}${removed_tags[*]}${NC}]") fi if [[ ${#common_tags[@]} -gt 0 ]]; then change_parts+=("kept [${GRAY}${common_tags[*]}${NC}]") fi change_details=$(IFS=', '; echo "${change_parts[*]}") fi log_change "${type^^} ${CYAN}${vmid}${NC}: ${change_details}" if [[ "$type" == "lxc" ]]; then pct set "${vmid}" -tags "$(IFS=';'; echo "${next_tags[*]}")" &>/dev/null else local vm_config="/etc/pve/qemu-server/${vmid}.conf" if [[ -f "$vm_config" ]]; then sed -i '/^tags:/d' "$vm_config" if [[ ${#next_tags[@]} -gt 0 ]]; then echo "tags: $(IFS=';'; echo "${next_tags[*]}")" >> "$vm_config" fi fi fi else # Tags unchanged local ip_count=${#formatted_ips[@]} local status_msg="" if [[ $ip_count -eq 0 ]]; then status_msg="No IPs detected" elif [[ $ip_count -eq 1 ]]; then status_msg="IP tag [${GRAY}${formatted_ips[0]}${NC}] unchanged" else status_msg="${ip_count} IP tags [${GRAY}${formatted_ips[*]}${NC}] unchanged" fi log_unchanged "${type^^} ${GRAY}${vmid}${NC}: ${status_msg}" fi } # Update all instances of specified type update_all_tags() { local type="$1" vmids count=0 if [[ "$type" == "lxc" ]]; then vmids=($(pct list 2>/dev/null | grep -v VMID | awk '{print $1}')) else local all_vm_configs=($(ls /etc/pve/qemu-server/*.conf 2>/dev/null | sed 's/.*\/\([0-9]*\)\.conf/\1/' | sort -n)) vmids=("${all_vm_configs[@]}") fi count=${#vmids[@]} [[ $count -eq 0 ]] && return # Display processing header with color if [[ "$type" == "lxc" ]]; then log_info "Processing ${WHITE}${count}${NC} LXC container(s) sequentially" else log_info "Processing ${WHITE}${count}${NC} virtual machine(s) sequentially" fi # Process each VM/LXC container sequentially for vmid in "${vmids[@]}"; do update_tags "$type" "$vmid" done # Add completion message if [[ "$type" == "lxc" ]]; then log_success "Completed processing LXC containers" else log_success "Completed processing virtual machines" fi } # Check if status changed check_status_changed() { local type="$1" current case "$type" in "lxc") current=$(pct list 2>/dev/null | grep -v VMID) ;; "vm") current=$(ls -la /etc/pve/qemu-server/*.conf 2>/dev/null) ;; "fw") current=$(ip link show type bridge 2>/dev/null) ;; esac local last_var="last_${type}_status" [[ "${!last_var}" == "$current" ]] && return 1 eval "$last_var='$current'" return 0 } # Main check function check() { local current_time=$(date +%s) # Simple periodic check - always update both LXC and VM every loop log_info "Starting periodic check" # Update LXC containers update_all_tags "lxc" # Update VMs update_all_tags "vm" } # Main loop main() { # Display startup message echo -e "\n${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" log_success "IP-Tag service started successfully" echo -e "${BLUE}ℹ${NC} Loop interval: ${WHITE}${LOOP_INTERVAL:-300}${NC} seconds" echo -e "${BLUE}ℹ${NC} Debug mode: ${WHITE}${DEBUG:-false}${NC}" echo -e "${BLUE}ℹ${NC} Tag format: ${WHITE}${TAG_FORMAT:-full}${NC}" echo -e "${BLUE}ℹ${NC} Allowed CIDRs: ${WHITE}${CIDR_LIST[*]}${NC}" echo -e "${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" if [[ "$FORCE_SINGLE_RUN" == "true" ]]; then check exit 0 fi while true; do check sleep "${LOOP_INTERVAL:-300}" done } # Simple LXC IP detection get_lxc_ips() { local vmid=$1 debug_log "lxc $vmid: starting IP detection" # Check if LXC is running local lxc_status=$(pct status "${vmid}" 2>/dev/null | awk '{print $2}') if [[ "$lxc_status" != "running" ]]; then debug_log "lxc $vmid: not running (status: $lxc_status)" return fi local ips="" # Method 1: Check Proxmox config for static IP local pve_lxc_config="/etc/pve/lxc/${vmid}.conf" if [[ -f "$pve_lxc_config" ]]; then local static_ip=$(grep -E "^net[0-9]+:" "$pve_lxc_config" 2>/dev/null | grep -oE 'ip=([0-9]{1,3}\.){3}[0-9]{1,3}' | cut -d'=' -f2 | head -1) if [[ -n "$static_ip" && "$static_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then debug_log "lxc $vmid: found static IP $static_ip in config" ips="$static_ip" fi fi # Method 2: ARP table lookup if no static IP if [[ -z "$ips" && -f "$pve_lxc_config" ]]; then local mac_addr=$(grep -Eo 'hwaddr=([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' "$pve_lxc_config" | head -1 | cut -d'=' -f2) if [[ -n "$mac_addr" ]]; then local bridge_name=$(grep -Eo 'bridge=[^,]+' "$pve_lxc_config" | head -1 | cut -d'=' -f2) local arp_ip=$(ip neighbor show | grep "$mac_addr" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1) if [[ -n "$arp_ip" && "$arp_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then debug_log "lxc $vmid: found IP $arp_ip via ARP table" ips="$arp_ip" fi fi fi # Method 3: Direct container command if ARP failed if [[ -z "$ips" ]]; then local container_ip=$(timeout 5s pct exec "$vmid" -- ip -4 addr show 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | grep -v '127.0.0.1' | head -1) if [[ -n "$container_ip" ]] && is_valid_ipv4 "$container_ip"; then debug_log "lxc $vmid: found IP $container_ip via pct exec" ips="$container_ip" fi fi debug_log "lxc $vmid: final IPs: '$ips'" echo "$ips" } main EOF } # Choose installation/update mode echo -e "\n${YW}Choose action:${CL}" echo -e "${GN}1)${CL} Install with automatic service (recommended)" echo -e "${GN}2)${CL} Install command only (manual execution)" echo -e "${GN}3)${CL} Update existing installation" echo -e "${RD}4)${CL} Cancel" while true; do read -p "Enter your choice (1-4): " choice case $choice in 1) INSTALL_MODE="service" echo -e "${GN}✓ Selected: Service installation${CL}" break ;; 2) INSTALL_MODE="command" echo -e "${GN}✓ Selected: Command-only installation${CL}" break ;; 3) echo -e "${GN}✓ Selected: Update installation${CL}" update_installation exit 0 ;; 4) msg_error "Action cancelled." exit 0 ;; *) msg_error "Please enter 1, 2, 3, or 4." ;; esac done echo -e "\n${YW}This will install ${APP} on ${hostname} in $INSTALL_MODE mode.${CL}" while true; do read -p "Proceed? (y/n): " yn case $yn in [Yy]*) break ;; [Nn]*) msg_error "Installation cancelled." exit ;; *) msg_error "Please answer yes or no." ;; esac done if ! pveversion | grep -Eq "pve-manager/(8\.[0-4]|9\.[0-9]+)(\.[0-9]+)*"; then msg_error "This version of Proxmox Virtual Environment is not supported" msg_error "⚠ Requires Proxmox Virtual Environment Version 8.0–8.4 or 9.x." msg_error "Exiting..." sleep 2 exit fi msg_info "Installing Dependencies" apt-get update &>/dev/null apt-get install -y ipcalc net-tools &>/dev/null msg_ok "Installed Dependencies" # Execute installation based on selected mode if [[ "$INSTALL_MODE" == "service" ]]; then # Full service installation msg_info "Setting up IP-Tag Scripts" mkdir -p /opt/iptag msg_ok "Setup IP-Tag Scripts" # Migrate config if needed migrate_config # Interactive configuration setup if [[ ! -f /opt/iptag/iptag.conf ]]; then interactive_config_setup msg_info "Setup Default Config" generate_config >/opt/iptag/iptag.conf msg_ok "Setup default config" else stop_spinner echo -e "\n${YW}Configuration file already exists.${CL}" read -p "Do you want to reconfigure tag format and loop interval? (y/n): " reconfigure case $reconfigure in [Yy]*) interactive_config_setup msg_info "Updating Configuration" generate_config >/opt/iptag/iptag.conf msg_ok "Updated configuration file" ;; *) msg_ok "Keeping existing configuration file" ;; esac fi msg_info "Setup Main Function" generate_main_script >/opt/iptag/iptag chmod +x /opt/iptag/iptag msg_ok "Setup Main Function" msg_info "Creating Service" generate_service >/lib/systemd/system/iptag.service msg_ok "Created Service" msg_info "Starting Service" systemctl daemon-reload &>/dev/null systemctl enable -q --now iptag.service &>/dev/null msg_ok "Started Service" msg_info "Creating manual run command" cat <<'EOF' >/usr/local/bin/iptag-run #!/usr/bin/env bash CONFIG_FILE="/opt/iptag/iptag.conf" SCRIPT_FILE="/opt/iptag/iptag" if [[ ! -f "$SCRIPT_FILE" ]]; then echo "✗ Main script not found: $SCRIPT_FILE" exit 1 fi export FORCE_SINGLE_RUN=true exec "$SCRIPT_FILE" EOF chmod +x /usr/local/bin/iptag-run msg_ok "Created iptag-run command" echo -e "\n${GN}${APP} service installation completed successfully! ${CL}" echo -e "${YW}The service is now running automatically.${CL}" echo -e "${YW}You can also run it manually with: ${GN}iptag-run${CL}\n" # Show configuration information show_post_install_info elif [[ "$INSTALL_MODE" == "command" ]]; then # Command-only installation install_command_only stop_spinner echo -e "\n${GN}${APP} command installation completed successfully! ${CL}" # Show configuration information show_post_install_info fi # Clean up any running spinner and exit stop_spinner exit 0